From 41240cad956f4148291c28c4c25775de27cc874e Mon Sep 17 00:00:00 2001 From: Nel <57587152+nelind3@users.noreply.github.com> Date: Fri, 21 Jun 2024 01:02:59 +0200 Subject: [PATCH 01/37] fix: #2316 (#2317) --- .../inspection/SpongeInjectionInspection.kt | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt b/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt index 7387d5d5a..76c14eb02 100644 --- a/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt +++ b/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt @@ -244,11 +244,13 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { ) } } - "ninja.leaping.configurate.loader.ConfigurationLoader" -> { + "ninja.leaping.configurate.loader.ConfigurationLoader", + "org.spongepowered.configurate.reference.ConfigurationReference", + "org.spongepowered.configurate.loader.ConfigurationLoader" -> { if (defaultConfig == null) { holder.registerProblem( variable.nameIdentifier ?: variable, - "Injected ConfigurationLoader must be annotated with @DefaultConfig.", + "Injected ${classType.name} must be annotated with @DefaultConfig.", ProblemHighlightType.GENERIC_ERROR, AddAnnotationFix(SpongeConstants.DEFAULT_CONFIG_ANNOTATION, annotationsOwner), ) @@ -257,7 +259,7 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { if (configDir != null) { holder.registerProblem( configDir, - "Injected ConfigurationLoader cannot be annotated with @ConfigDir.", + "Injected ${classType.name} cannot be annotated with @ConfigDir.", ProblemHighlightType.GENERIC_ERROR, QuickFixFactory.getInstance().createDeleteFix(configDir, "Remove @ConfigDir"), ) @@ -267,7 +269,7 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { val ref = classType.reference holder.registerProblem( ref, - "Injected ConfigurationLoader must have a generic parameter.", + "Injected ${classType.name} must have a generic parameter.", ProblemHighlightType.GENERIC_ERROR, MissingConfLoaderTypeParamFix(ref), ) @@ -275,14 +277,17 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { classType.parameters.firstOrNull()?.let { param -> val paramType = param as? PsiClassReferenceType ?: return@let val paramTypeFQName = paramType.fullQualifiedName ?: return@let - if (paramTypeFQName != "ninja.leaping.configurate.commented.CommentedConfigurationNode") { + if ( + paramTypeFQName != "ninja.leaping.configurate.commented.CommentedConfigurationNode" && + paramTypeFQName != "org.spongepowered.configurate.CommentedConfigurationNode" + ) { val ref = param.reference holder.registerProblem( ref, "Injected ConfigurationLoader generic parameter must be " + "CommentedConfigurationNode.", ProblemHighlightType.GENERIC_ERROR, - WrongConfLoaderTypeParamFix(ref), + WrongConfLoaderTypeParamFix(classType.className, ref), ) } } @@ -371,7 +376,8 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { } } - class WrongConfLoaderTypeParamFix(ref: PsiJavaCodeReferenceElement) : LocalQuickFixOnPsiElement(ref) { + class WrongConfLoaderTypeParamFix(private val clazzName: String, param: PsiJavaCodeReferenceElement) : + LocalQuickFixOnPsiElement(param) { override fun getFamilyName(): String = name @@ -379,7 +385,11 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { val newRef = JavaPsiFacade.getElementFactory(project).createReferenceFromText( - "ninja.leaping.configurate.commented.CommentedConfigurationNode", + when (clazzName) { + "ninja.leaping.configurate.loader.ConfigurationLoader" -> + "ninja.leaping.configurate.commented.CommentedConfigurationNode" + else -> { "org.spongepowered.configurate.CommentedConfigurationNode" } + }, startElement, ) startElement.replace(newRef) @@ -393,11 +403,23 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { override fun getText(): String = "Insert generic parameter" override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { - val newRef = JavaPsiFacade.getElementFactory(project).createReferenceFromText( - "ninja.leaping.configurate.loader.ConfigurationLoader" + - "", - startElement, - ) + val newRef: PsiElement = if ( + JavaPsiFacade.getInstance(project) + .findPackage("ninja.leaping.configurate") != null + ) { + JavaPsiFacade.getElementFactory(project).createReferenceFromText( + "ninja.leaping.configurate.loader.ConfigurationLoader" + + "", + startElement + ) + } else { + JavaPsiFacade.getElementFactory(project).createReferenceFromText( + "org.spongepowered.configurate.loader.ConfigurationLoader" + + "", + startElement + ) + } + startElement.replace(newRef) } } From 6de337673004d203fd7ee14f891ea16f5c75c697 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Fri, 21 Jun 2024 10:04:26 +0200 Subject: [PATCH 02/37] Initial support for NeoForge's ModDevGradle Mappings don't work currently, the TSRG file seems to contain indices to an array in another JSON file? --- .../NeoModDevGradleModelBuilderImpl.groovy | 75 ++++++++++++++++++ .../neomoddev/NeoModDevGradleModelImpl.groovy | 43 +++++++++++ .../mcp/gradle/tooling/McpModelNMD.java | 30 ++++++++ ...plugins.gradle.tooling.ModelBuilderService | 1 + .../mcp/gradle/McpProjectResolverExtension.kt | 6 +- .../gradle/datahandler/McpModelNMDHandler.kt | 76 +++++++++++++++++++ 6 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy create mode 100644 src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy create mode 100644 src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java create mode 100644 src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy new file mode 100644 index 000000000..02ef8e305 --- /dev/null +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy @@ -0,0 +1,75 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.gradle.tooling.neomoddev + +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD +import org.gradle.api.Project +import org.jetbrains.annotations.NotNull +import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder +import org.jetbrains.plugins.gradle.tooling.ModelBuilderService + +import java.nio.file.Files + +final class NeoModDevGradleModelBuilderImpl implements ModelBuilderService { + + @Override + boolean canBuild(String modelName) { + return McpModelNMD.name == modelName + } + + @Override + Object buildAll(String modelName, Project project) { + def extension = project.extensions.findByName('neoForge') + if (extension == null) { + return null + } + + if (!project.plugins.findPlugin("net.neoforged.moddev")) { + return null + } + + def neoforgeVersion = extension.version.get() + if (neoforgeVersion == null) { + return null + } + + def accessTransformers = extension.accessTransformers.get().collect { project.file(it) } + + // Hacky way to guess where the mappings file is, but I could not find a proper way to find it + def neoformDir = project.buildDir.toPath().resolve("neoForm") + def mappingsFile = Files.list(neoformDir) + .map { it.resolve("config/joined.tsrg") } + .filter { Files.exists(it) } + .findFirst() + .orElse(null) + ?.toFile() + + //noinspection GroovyAssignabilityCheck + return new NeoModDevGradleModelImpl(neoforgeVersion, mappingsFile, accessTransformers) + } + + @Override + ErrorMessageBuilder getErrorMessageBuilder(@NotNull Project project, @NotNull Exception e) { + return ErrorMessageBuilder.create( + project, e, "MinecraftDev import errors" + ).withDescription("Unable to build MinecraftDev MCP project configuration") + } +} diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy new file mode 100644 index 000000000..5640af6fd --- /dev/null +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy @@ -0,0 +1,43 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.gradle.tooling.neomoddev + + +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD +import groovy.transform.CompileStatic + +@CompileStatic +final class NeoModDevGradleModelImpl implements McpModelNMD, Serializable { + + final String neoForgeVersion + final File mappingsFile + final List accessTransformers + + NeoModDevGradleModelImpl( + final String neoForgeVersion, + final File mappingsFile, + final List accessTransformers + ) { + this.neoForgeVersion = neoForgeVersion + this.mappingsFile = mappingsFile + this.accessTransformers = accessTransformers + } +} diff --git a/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java new file mode 100644 index 000000000..beff8af28 --- /dev/null +++ b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.gradle.tooling; + +import java.io.File; +import java.util.List; + +public interface McpModelNMD { + String getNeoForgeVersion(); + File getMappingsFile(); + List getAccessTransformers(); +} diff --git a/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService b/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService index 382ea8362..9746eaf54 100644 --- a/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService +++ b/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService @@ -1,6 +1,7 @@ com.demonwav.mcdev.platform.mcp.gradle.tooling.archloom.ArchitecturyModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.fabricloom.FabricLoomModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.neogradle.NeoGradle7ModelBuilderImpl +com.demonwav.mcdev.platform.mcp.gradle.tooling.neomoddev.NeoModDevGradleModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.vanillagradle.VanillaGradleModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG2BuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG3BuilderImpl diff --git a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt index 53b90142a..5d45a3d79 100644 --- a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt +++ b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt @@ -23,9 +23,11 @@ package com.demonwav.mcdev.platform.mcp.gradle import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelFG2Handler import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelFG3Handler import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelNG7Handler +import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelNMDHandler import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG2 import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG3 import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNG7 +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD import com.demonwav.mcdev.util.runGradleTask import com.intellij.openapi.externalSystem.model.DataNode import com.intellij.openapi.externalSystem.model.project.ModuleData @@ -38,7 +40,7 @@ class McpProjectResolverExtension : AbstractProjectResolverExtension() { // Register our custom Gradle tooling API model in IntelliJ's project resolver override fun getExtraProjectModelClasses(): Set> = - setOf(McpModelFG2::class.java, McpModelFG3::class.java, McpModelNG7::class.java) + setOf(McpModelFG2::class.java, McpModelFG3::class.java, McpModelNG7::class.java, McpModelNMD::class.java) override fun getToolingExtensionsClasses() = extraProjectModelClasses @@ -91,6 +93,6 @@ class McpProjectResolverExtension : AbstractProjectResolverExtension() { } companion object { - private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler, McpModelNG7Handler) + private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler, McpModelNG7Handler, McpModelNMDHandler) } } diff --git a/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt new file mode 100644 index 000000000..ca496ad49 --- /dev/null +++ b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt @@ -0,0 +1,76 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mcp.gradle.datahandler + +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.at.AtFileType +import com.demonwav.mcdev.platform.mcp.gradle.McpModelData +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD +import com.demonwav.mcdev.platform.mcp.srg.SrgType +import com.demonwav.mcdev.util.runWriteTaskLater +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.fileTypes.ExactFileNameMatcher +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.vfs.LocalFileSystem +import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData +import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext + +object McpModelNMDHandler : McpModelDataHandler { + + override fun build( + gradleModule: IdeaModule, + node: DataNode, + resolverCtx: ProjectResolverContext, + ) { + val data = resolverCtx.getExtraProject(gradleModule, McpModelNMD::class.java) ?: return + + val state = McpModuleSettings.State( + "1." + data.neoForgeVersion.substringBefore('.'), + null, + data.mappingsFile?.absolutePath, + SrgType.TSRG, + data.neoForgeVersion, + ) + + val ats = data.accessTransformers + if (ats != null && ats.isNotEmpty()) { + runWriteTaskLater { + for (at in ats) { + val fileTypeManager = FileTypeManager.getInstance() + val atFile = LocalFileSystem.getInstance().findFileByIoFile(at) ?: continue + fileTypeManager.associate(AtFileType, ExactFileNameMatcher(atFile.name)) + } + } + } + + val modelData = McpModelData(node.data, state, null, data.accessTransformers) + node.createChild(McpModelData.KEY, modelData) + + for (child in node.children) { + val childData = child.data + if (childData is GradleSourceSetData) { + child.createChild(McpModelData.KEY, modelData.copy(module = childData)) + } + } + } +} From 9da38412022ada1eb86303c955939d4f0dee67cb Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 2 Jul 2024 14:19:07 +0200 Subject: [PATCH 03/37] Improve fabric.mod.json entrypoints insight - Recognize entrypoints declared in object form - Add more conditions to the inspection - Add some tests to cover the inspection Fixes #2296 --- build.gradle.kts | 2 + gradle/libs.versions.toml | 1 + .../inspection/FabricEntrypointsInspection.kt | 48 +++- .../fabric/reference/EntryPointReference.kt | 54 ++-- .../reference/FabricReferenceContributor.kt | 26 +- .../fabric/reference/ResourceFileReference.kt | 40 ++- src/test/kotlin/framework/ProjectBuilder.kt | 6 + .../fabric/FabricEntrypointsInspectionTest.kt | 264 ++++++++++++++++++ 8 files changed, 396 insertions(+), 45 deletions(-) create mode 100644 src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index f92d06dc6..fd9b13534 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,6 +83,7 @@ repositories { maven("https://maven.fabricmc.net/") { content { includeModule("net.fabricmc", "mapping-io") + includeModule("net.fabricmc", "fabric-loader") } } mavenCentral() @@ -119,6 +120,7 @@ dependencies { classifier = "shaded" } } + testLibs(libs.test.fabricloader) testLibs(libs.test.nbt) { artifact { extension = "nbt" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abc4aad82..f8dabc6d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", versio test-mockJdk = "org.jetbrains.idea:mock-jdk:1.7-4d76c50" test-mixin = "org.spongepowered:mixin:0.8.5" test-spongeapi = "org.spongepowered:spongeapi:7.4.0" +test-fabricloader = "net.fabricmc:fabric-loader:0.15.11" test-nbt = "com.demonwav.mcdev:all-types-nbt:1.0" junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } diff --git a/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt index 3ecde42dd..7092fca87 100644 --- a/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt +++ b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt @@ -22,12 +22,15 @@ package com.demonwav.mcdev.platform.fabric.inspection import com.demonwav.mcdev.platform.fabric.reference.EntryPointReference import com.demonwav.mcdev.platform.fabric.util.FabricConstants +import com.demonwav.mcdev.util.equivalentTo import com.intellij.codeInspection.InspectionManager import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.json.psi.JsonArray import com.intellij.json.psi.JsonElementVisitor +import com.intellij.json.psi.JsonLiteral import com.intellij.json.psi.JsonProperty import com.intellij.json.psi.JsonStringLiteral import com.intellij.psi.JavaPsiFacade @@ -79,8 +82,7 @@ class FabricEntrypointsInspection : LocalInspectionTool() { val element = resolved.singleOrNull()?.element when { element is PsiClass && !literal.text.contains("::") -> { - val propertyKey = literal.parentOfType()?.name - val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } + val (propertyKey, expectedType) = findEntrypointKeyAndType(literal) if (propertyKey != null && expectedType != null && !isEntrypointOfCorrectType(element, propertyKey) ) { @@ -111,21 +113,43 @@ class FabricEntrypointsInspection : LocalInspectionTool() { reference.rangeInElement, ) } + + if (!element.hasModifierProperty(PsiModifier.PUBLIC)) { + holder.registerProblem( + literal, + "Entrypoint method must be public", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + + if (!element.hasModifierProperty(PsiModifier.STATIC)) { + val clazz = element.containingClass + if (clazz != null && clazz.constructors.isNotEmpty() && + clazz.constructors.find { !it.hasParameters() } == null + ) { + holder.registerProblem( + literal, + "Entrypoint instance method class must have an empty constructor", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + } } element is PsiField -> { - if (!element.hasModifierProperty(PsiModifier.STATIC)) { + if (!element.hasModifierProperty(PsiModifier.PUBLIC)) { holder.registerProblem( literal, - "Entrypoint field must be static", + "Entrypoint field must be public", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, reference.rangeInElement, ) } - val propertyKey = literal.parentOfType()?.name + val (propertyKey, expectedType) = findEntrypointKeyAndType(literal) val fieldTypeClass = (element.type as? PsiClassType)?.resolve() - val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } if (propertyKey != null && fieldTypeClass != null && expectedType != null && !isEntrypointOfCorrectType(fieldTypeClass, propertyKey) ) { @@ -141,11 +165,21 @@ class FabricEntrypointsInspection : LocalInspectionTool() { } } + private fun findEntrypointKeyAndType(literal: JsonLiteral): Pair { + val propertyKey = when (val parent = literal.parent) { + is JsonArray -> (parent.parent as? JsonProperty)?.name + is JsonProperty -> parent.parentOfType()?.name + else -> null + } + val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } + return propertyKey to expectedType + } + private fun isEntrypointOfCorrectType(element: PsiClass, type: String): Boolean { val entrypointClass = FabricConstants.ENTRYPOINT_BY_TYPE[type] ?: return false val clazz = JavaPsiFacade.getInstance(element.project).findClass(entrypointClass, element.resolveScope) - return clazz != null && element.isInheritor(clazz, true) + return clazz != null && (element.equivalentTo(clazz) || element.isInheritor(clazz, true)) } } } diff --git a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt index 796058d64..1b8041c8a 100644 --- a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt +++ b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt @@ -25,6 +25,8 @@ import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.manipulator import com.demonwav.mcdev.util.reference.InspectionReference import com.intellij.codeInsight.completion.JavaLookupElementBuilder +import com.intellij.json.psi.JsonArray +import com.intellij.json.psi.JsonProperty import com.intellij.json.psi.JsonStringLiteral import com.intellij.openapi.util.TextRange import com.intellij.psi.JavaPsiFacade @@ -40,7 +42,9 @@ import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceBase import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.ResolveResult +import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.searches.ClassInheritorsSearch +import com.intellij.psi.util.parentOfType import com.intellij.util.ArrayUtil import com.intellij.util.IncorrectOperationException import com.intellij.util.ProcessingContext @@ -136,27 +140,17 @@ object EntryPointReference : PsiReferenceProvider() { fun isEntryPointReference(reference: PsiReference) = reference is Reference - fun isValidEntrypointClass(element: PsiClass): Boolean { - val psiFacade = JavaPsiFacade.getInstance(element.project) - var inheritsEntrypointInterface = false - for (entrypoint in FabricConstants.ENTRYPOINTS) { - val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope) - ?: continue - if (element.isInheritor(entrypointClass, true)) { - inheritsEntrypointInterface = true - break - } - } - return inheritsEntrypointInterface + fun isValidEntrypointClass(element: PsiClass, entrypointClass: PsiClass): Boolean { + return element.isInheritor(entrypointClass, true) } - fun isValidEntrypointField(field: PsiField): Boolean { + fun isValidEntrypointField(field: PsiField, entrypointClass: PsiClass): Boolean { if (!field.hasModifierProperty(PsiModifier.PUBLIC) || !field.hasModifierProperty(PsiModifier.STATIC)) { return false } val fieldTypeClass = (field.type as? PsiClassType)?.resolve() - return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass) + return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass, entrypointClass) } fun isValidEntrypointMethod(method: PsiMethod): Boolean { @@ -228,30 +222,36 @@ object EntryPointReference : PsiReferenceProvider() { val text = element.text.substring(range.startOffset, range.endOffset) val parts = text.split("::", limit = 2) + val psiFacade = JavaPsiFacade.getInstance(element.project) + val entrypointType = getEntrypointType()?.let(FabricConstants.ENTRYPOINT_BY_TYPE::get) + ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val entrypointClass = psiFacade.findClass(entrypointType, element.resolveScope) + ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val variants = mutableListOf() if (!isMemberReference) { - val psiFacade = JavaPsiFacade.getInstance(element.project) - for (entrypoint in FabricConstants.ENTRYPOINTS) { - val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope) - ?: continue - ClassInheritorsSearch.search(entrypointClass, true) - .mapNotNullTo(variants) { - val shortName = it.name ?: return@mapNotNullTo null - val fqName = it.fullQualifiedName ?: return@mapNotNullTo null - JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName) - } - } + val scope = element.resolveScope.intersectWith(GlobalSearchScope.projectScope(element.project)) + ClassInheritorsSearch.search(entrypointClass, scope, true) + .mapNotNullTo(variants) { + val shortName = it.name ?: return@mapNotNullTo null + val fqName = it.fullQualifiedName ?: return@mapNotNullTo null + JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName) + } } else if (parts.size >= 2) { - val psiFacade = JavaPsiFacade.getInstance(element.project) val className = parts[0].replace('$', '.') val clazz = psiFacade.findClass(className, element.resolveScope) if (clazz != null) { - clazz.fields.filterTo(variants, ::isValidEntrypointField) + clazz.fields.filterTo(variants) { isValidEntrypointField(it, entrypointClass) } clazz.methods.filterTo(variants, ::isValidEntrypointMethod) } } return variants.toTypedArray() } + + private fun getEntrypointType(): String? { + val entrypointsProperty = element.parentOfType()?.parent as? JsonProperty + return entrypointsProperty?.name + } } } diff --git a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt index 896da285e..758e3e890 100644 --- a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt +++ b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt @@ -23,9 +23,11 @@ package com.demonwav.mcdev.platform.fabric.reference import com.demonwav.mcdev.platform.fabric.util.FabricConstants import com.demonwav.mcdev.util.isPropertyValue import com.intellij.json.psi.JsonArray +import com.intellij.json.psi.JsonElement import com.intellij.json.psi.JsonObject import com.intellij.json.psi.JsonStringLiteral import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.StandardPatterns import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceRegistrar @@ -34,19 +36,25 @@ class FabricReferenceContributor : PsiReferenceContributor() { val stringInModJson = PlatformPatterns.psiElement(JsonStringLiteral::class.java) .inVirtualFile(PlatformPatterns.virtualFile().withName(FabricConstants.FABRIC_MOD_JSON)) - val entryPointPattern = stringInModJson.withParent( - PlatformPatterns.psiElement(JsonArray::class.java) - .withSuperParent( - 2, - PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints"), - ), - ) + val entrypointsArray = PlatformPatterns.psiElement(JsonArray::class.java) + .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints")) + val entryPointSimplePattern = stringInModJson.withParent(entrypointsArray) + val entryPointObjectPattern = stringInModJson.isPropertyValue("value") + .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).withParent(entrypointsArray)) + val entryPointPattern = StandardPatterns.or(entryPointSimplePattern, entryPointObjectPattern) registrar.registerReferenceProvider(entryPointPattern, EntryPointReference) - val mixinConfigPattern = stringInModJson.withParent( + val mixinConfigSimplePattern = stringInModJson.withParent( PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins"), ) - registrar.registerReferenceProvider(mixinConfigPattern, ResourceFileReference("mixin config '%s'")) + val mixinsConfigArray = PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins") + val mixinConfigObjectPattern = stringInModJson.isPropertyValue("config") + .withSuperParent(2, PlatformPatterns.psiElement(JsonElement::class.java).withParent(mixinsConfigArray)) + val mixinConfigPattern = StandardPatterns.or(mixinConfigSimplePattern, mixinConfigObjectPattern) + registrar.registerReferenceProvider( + mixinConfigPattern, + ResourceFileReference("mixin config '%s'", Regex("(.+)\\.mixins\\.json")) + ) registrar.registerReferenceProvider( stringInModJson.isPropertyValue("accessWidener"), diff --git a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt index 834ae6e0c..1088b03e9 100644 --- a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt +++ b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt @@ -27,8 +27,12 @@ import com.demonwav.mcdev.util.manipulator import com.demonwav.mcdev.util.mapFirstNotNull import com.demonwav.mcdev.util.reference.InspectionReference import com.intellij.json.psi.JsonStringLiteral +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.rootManager import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.vfs.findPsiFile import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager @@ -37,13 +41,17 @@ import com.intellij.psi.PsiReferenceBase import com.intellij.psi.PsiReferenceProvider import com.intellij.util.IncorrectOperationException import com.intellij.util.ProcessingContext +import org.jetbrains.jps.model.java.JavaResourceRootType -class ResourceFileReference(private val description: String) : PsiReferenceProvider() { +class ResourceFileReference( + private val description: String, + private val filenamePattern: Regex? = null +) : PsiReferenceProvider() { override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array { return arrayOf(Reference(description, element as JsonStringLiteral)) } - private class Reference(desc: String, element: JsonStringLiteral) : + private inner class Reference(desc: String, element: JsonStringLiteral) : PsiReferenceBase(element), InspectionReference { override val description = desc @@ -61,6 +69,9 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi ?: ModuleRootManager.getInstance(module) .getDependencies(false) .mapFirstNotNull(::findFileIn) + ?: ModuleManager.getInstance(element.project) + .getModuleDependentModules(module) + .mapFirstNotNull(::findFileIn) } override fun bindToElement(newTarget: PsiElement): PsiElement? { @@ -70,5 +81,30 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi val manipulator = element.manipulator ?: return null return manipulator.handleContentChange(element, manipulator.getRangeInElement(element), newTarget.name) } + + override fun getVariants(): Array { + if (filenamePattern == null) { + return emptyArray() + } + + val module = element.findModule() ?: return emptyArray() + val variants = mutableListOf() + val relevantModules = ModuleManager.getInstance(element.project).getModuleDependentModules(module) + module + runReadAction { + val relevantRoots = relevantModules.flatMap { + it.rootManager.getSourceRoots(JavaResourceRootType.RESOURCE) + } + for (roots in relevantRoots) { + for (child in roots.children) { + val relativePath = child.path.removePrefix(roots.path) + val testRelativePath = "/$relativePath" + if (testRelativePath.matches(filenamePattern)) { + variants.add(child.findPsiFile(element.project) ?: relativePath) + } + } + } + } + return variants.toTypedArray() + } } } diff --git a/src/test/kotlin/framework/ProjectBuilder.kt b/src/test/kotlin/framework/ProjectBuilder.kt index 45e4dd2ed..07fdc5edf 100644 --- a/src/test/kotlin/framework/ProjectBuilder.kt +++ b/src/test/kotlin/framework/ProjectBuilder.kt @@ -63,6 +63,12 @@ class ProjectBuilder(private val fixture: JavaCodeInsightTestFixture, private va configure: Boolean = true, allowAst: Boolean = false, ) = file(path, code, ".nbtt", configure, allowAst) + fun json( + path: String, + @Language("JSON") code: String, + configure: Boolean = true, + allowAst: Boolean = false, + ) = file(path, code, ".json", configure, allowAst) inline fun dir(path: String, block: ProjectBuilder.() -> Unit) { val oldIntermediatePath = intermediatePath diff --git a/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt new file mode 100644 index 000000000..619fc4ad8 --- /dev/null +++ b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt @@ -0,0 +1,264 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.fabric + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.framework.EdtInterceptor +import com.demonwav.mcdev.framework.ProjectBuilder +import com.demonwav.mcdev.framework.createLibrary +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.fabric.inspection.FabricEntrypointsInspection +import com.demonwav.mcdev.util.runWriteTask +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.roots.libraries.Library +import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(EdtInterceptor::class) +@DisplayName("Fabric Entrypoints Inspection Tests") +class FabricEntrypointsInspectionTest : BaseMinecraftTest(PlatformType.FABRIC) { + + private var library: Library? = null + + @BeforeEach + fun initFabric() { + runWriteTask { + library = createLibrary(project, "fabric-loader") + } + + ModuleRootModificationUtil.updateModel(module) { model -> + model.addLibraryEntry(library ?: throw IllegalStateException("Library not created")) + } + } + + @AfterEach + fun cleanupFabric() { + library?.let { l -> + ModuleRootModificationUtil.updateModel(module) { model -> + model.removeOrderEntry( + model.findLibraryOrderEntry(l) ?: throw IllegalStateException("Library not found"), + ) + } + + runWriteTask { + val table = LibraryTablesRegistrar.getInstance().getLibraryTable(project) + table.modifiableModel.let { model -> + model.removeLibrary(l) + model.commit() + } + } + } + } + + private fun doTest(@Language("JSON") json: String, builder: (ProjectBuilder.() -> Unit) = {}) { + buildProject { + java( + "GoodSimpleModInitializer.java", + """ + import net.fabricmc.api.ModInitializer; + + public class GoodSimpleModInitializer implements ModInitializer { + @Override + public void onInitialize() { + } + + public void handle() {} + } + """.trimIndent() + ) + java( + "GoodSimpleClientModInitializer.java", + """ + import net.fabricmc.api.ClientModInitializer; + + public class GoodSimpleClientModInitializer implements ClientModInitializer { + @Override + public void onInitializeClient() { + } + } + """.trimIndent() + ) + java( + "BadSimpleModInitializer.java", + """ + public class BadSimpleModInitializer { + public void handle(String param) {} + } + """.trimIndent() + ) + java( + "BadSimpleClientModInitializer.java", + """ + public class BadSimpleClientModInitializer {} + """.trimIndent() + ) + + builder() + + json("fabric.mod.json", json) + } + + fixture.enableInspections(FabricEntrypointsInspection::class) + fixture.checkHighlighting(false, false, false) + } + + @Test + fun validInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + { + "value": "GoodSimpleModInitializer" + }, + "GoodSimpleModInitializer::handle" + ], + "client": [ + "GoodSimpleClientModInitializer" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun invalidInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + "GoodSimpleClientModInitializer", + { + "value": "BadSimpleModInitializer" + } + ], + "client": [ + "BadSimpleClientModInitializer", + "GoodSimpleModInitializer" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun missingEmptyConstructor() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadCtorSimpleModInitializer" + ] + } + } + """.trimIndent() + ) { + java( + "BadCtorSimpleModInitializer.java", + """ + import net.fabricmc.api.ModInitializer; + + public class BadCtorSimpleModInitializer implements ModInitializer { + public BadCtorSimpleModInitializer(String param) {} + } + """.trimIndent() + ) + } + } + + @Test + fun entrypointMethodWithParameter() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadSimpleModInitializer::handle" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun entrypointInstanceMethodInClassWithNoEmptyCtor() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadTestInitializer::goodInitialize", + "BadTestInitializer::badInitialize" + ] + } + } + """.trimIndent() + ) { + java( + "BadTestInitializer.java", + """ + public class BadTestInitializer { + public BadTestInitializer(String param) {} + public static void goodInitialize() {} + public void badInitialize() {} + } + """.trimIndent() + ) + } + } + + @Test + fun entrypointFieldInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + "ModInitializerContainer::initializer", + "ModInitializerContainer::badTypeInitializer" + ] + } + } + """.trimIndent() + ) { + java( + "ModInitializerContainer.java", + """ + public class ModInitializerContainer { + public static GoodSimpleModInitializer initializer = new GoodSimpleModInitializer(); + public static String badTypeInitializer = "No..."; + } + """.trimIndent() + ) + } + } +} From d20f5e7f61d2f846778c86634cecdb282c8a8b9a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 3 Jul 2024 07:15:16 +0200 Subject: [PATCH 04/37] Extract lexer and parser tasks into their own classes This makes those tasks compatible with build and configuration cache Unfortunately two other issues prevent us from using the configuration cache, one is the licenser plugin, the other is the access of testLibs in the test task configuration --- build.gradle.kts | 5 +- buildSrc/src/main/kotlin/JFlexExec.kt | 84 +++++++++++++++++ buildSrc/src/main/kotlin/ParserExec.kt | 88 ++++++++++++++++++ buildSrc/src/main/kotlin/util.kt | 92 ++++--------------- .../grammars/TranslationTemplateLexer.flex | 2 +- .../TranslationTemplateLexerAdapter.kt | 2 +- 6 files changed, 198 insertions(+), 75 deletions(-) create mode 100644 buildSrc/src/main/kotlin/JFlexExec.kt create mode 100644 buildSrc/src/main/kotlin/ParserExec.kt diff --git a/build.gradle.kts b/build.gradle.kts index fd9b13534..3b2663f07 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -343,7 +343,10 @@ val generateNbttParser by parser("NbttParser", "com/demonwav/mcdev/nbt/lang/gen" val generateLangLexer by lexer("LangLexer", "com/demonwav/mcdev/translations/lang/gen") val generateLangParser by parser("LangParser", "com/demonwav/mcdev/translations/lang/gen") -val generateTranslationTemplateLexer by lexer("TranslationTemplateLexer", "com/demonwav/mcdev/translations/lang/gen") +val generateTranslationTemplateLexer by lexer( + "TranslationTemplateLexer", + "com/demonwav/mcdev/translations/template/gen" +) val generate by tasks.registering { group = "minecraft" diff --git a/buildSrc/src/main/kotlin/JFlexExec.kt b/buildSrc/src/main/kotlin/JFlexExec.kt new file mode 100644 index 000000000..dca469daa --- /dev/null +++ b/buildSrc/src/main/kotlin/JFlexExec.kt @@ -0,0 +1,84 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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 . + */ + +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.ConfigurableFileTree +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.JavaExec +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile + +abstract class JFlexExec : JavaExec() { + + @get:InputFile + abstract val sourceFile: RegularFileProperty + + @get:InputFiles + abstract val jflex: ConfigurableFileCollection + + @get:InputFile + abstract val skeletonFile: RegularFileProperty + + @get:OutputDirectory + abstract val destinationDirectory: DirectoryProperty + + @get:OutputFile + abstract val destinationFile: RegularFileProperty + + @get:Internal + abstract val logFile: RegularFileProperty + + @get:Inject + abstract val fs: FileSystemOperations + + init { + mainClass.set("jflex.Main") + } + + override fun exec() { + classpath = jflex + + args( + "--skel", skeletonFile.get().asFile.absolutePath, + "-d", destinationDirectory.get().asFile.absolutePath, + sourceFile.get().asFile.absolutePath + ) + + fs.delete { delete(destinationDirectory) } + + val taskOutput = ByteArrayOutputStream() + standardOutput = taskOutput + errorOutput = taskOutput + + super.exec() + + val log = logFile.get().asFile + log.parentFile.mkdirs() + log.writeBytes(taskOutput.toByteArray()) + } +} diff --git a/buildSrc/src/main/kotlin/ParserExec.kt b/buildSrc/src/main/kotlin/ParserExec.kt new file mode 100644 index 000000000..adb38256d --- /dev/null +++ b/buildSrc/src/main/kotlin/ParserExec.kt @@ -0,0 +1,88 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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 . + */ + +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.JavaExec +import org.gradle.api.tasks.OutputDirectory + +abstract class ParserExec : JavaExec() { + + @get:InputFile + abstract val sourceFile: RegularFileProperty + + @get:InputFiles + abstract val grammarKit: ConfigurableFileCollection + + @get:OutputDirectory + abstract val destinationRootDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val destinationDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val psiDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val parserDirectory: DirectoryProperty + + @get:Internal + abstract val logFile: RegularFileProperty + + @get:Inject + abstract val fs: FileSystemOperations + + init { + mainClass.set("org.intellij.grammar.Main") + + jvmArgs( + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED" + ) + } + + override fun exec() { + classpath = grammarKit + args( + destinationRootDirectory.get().asFile, + sourceFile.get().asFile + ) + + fs.delete { delete(psiDirectory, parserDirectory) } + + val taskOutput = ByteArrayOutputStream() + standardOutput = taskOutput + errorOutput = taskOutput + + super.exec() + + val log = logFile.get().asFile + log.parentFile.mkdirs() + log.writeBytes(taskOutput.toByteArray()) + } +} diff --git a/buildSrc/src/main/kotlin/util.kt b/buildSrc/src/main/kotlin/util.kt index 054a24cf7..7a6623b23 100644 --- a/buildSrc/src/main/kotlin/util.kt +++ b/buildSrc/src/main/kotlin/util.kt @@ -32,94 +32,42 @@ import org.gradle.kotlin.dsl.configure typealias TaskDelegate = RegisteringDomainObjectDelegateProviderWithTypeAndAction -fun Project.lexer(flex: String, pack: String): TaskDelegate { +fun Project.lexer(flex: String, pack: String): TaskDelegate { configure { exclude(pack.removeSuffix("/") + "/**") } - return tasks.registering(JavaExec::class) { - val src = layout.projectDirectory.file("src/main/grammars/$flex.flex") - val dst = layout.buildDirectory.dir("gen/$pack") - val output = layout.buildDirectory.file("gen/$pack/$flex.java") - val logOutout = layout.buildDirectory.file("logs/generate$flex.log") + return tasks.registering(JFlexExec::class) { + sourceFile.set(layout.projectDirectory.file("src/main/grammars/$flex.flex")) + destinationDirectory.set(layout.buildDirectory.dir("gen/$pack")) + destinationFile.set(layout.buildDirectory.file("gen/$pack/$flex.java")) + logFile.set(layout.buildDirectory.file("logs/generate$flex.log")) val jflex by project.configurations - val jflexSkeleton by project.configurations - - classpath = jflex - mainClass.set("jflex.Main") - - val taskOutput = ByteArrayOutputStream() - standardOutput = taskOutput - errorOutput = taskOutput - - doFirst { - args( - "--skel", jflexSkeleton.singleFile.absolutePath, - "-d", dst.get().asFile.absolutePath, - src.asFile.absolutePath - ) - - // Delete current lexer - project.delete(output) - logOutout.get().asFile.parentFile.mkdirs() - } - - doLast { - logOutout.get().asFile.writeBytes(taskOutput.toByteArray()) - } + this.jflex.setFrom(jflex) - inputs.files(src, jflexSkeleton) - outputs.file(output) + val jflexSkeleton by project.configurations + skeletonFile.set(jflexSkeleton.singleFile) } } -fun Project.parser(bnf: String, pack: String): TaskDelegate { +fun Project.parser(bnf: String, pack: String): TaskDelegate { configure { exclude(pack.removeSuffix("/") + "/**") } - return tasks.registering(JavaExec::class) { - val src = project.layout.projectDirectory.file("src/main/grammars/$bnf.bnf") - val dstRoot = project.layout.buildDirectory.dir("gen") - val dst = dstRoot.map { it.dir(pack) } - val psiDir = dst.map { it.dir("psi") } - val parserDir = dst.map { it.dir("parser") } - val logOutout = layout.buildDirectory.file("logs/generate$bnf.log") + return tasks.registering(ParserExec::class) { + val destRoot = project.layout.buildDirectory.dir("gen") + val dest = destRoot.map { it.dir(pack) } + sourceFile.set(project.layout.projectDirectory.file("src/main/grammars/$bnf.bnf")) + destinationRootDirectory.set(destRoot) + destinationDirectory.set(dest) + psiDirectory.set(dest.map { it.dir("psi") }) + parserDirectory.set(dest.map { it.dir("parser") }) + logFile.set(layout.buildDirectory.file("logs/generate$bnf.log")) val grammarKit by project.configurations - - val taskOutput = ByteArrayOutputStream() - standardOutput = taskOutput - errorOutput = taskOutput - - classpath = grammarKit - mainClass.set("org.intellij.grammar.Main") - - if (JavaVersion.current().isJava9Compatible) { - jvmArgs( - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", - "--add-opens", "java.base/java.util=ALL-UNNAMED" - ) - } - - doFirst { - project.delete(psiDir, parserDir) - args(dstRoot.get().asFile, src.asFile) - logOutout.get().asFile.parentFile.mkdirs() - } - doLast { - logOutout.get().asFile.writeBytes(taskOutput.toByteArray()) - } - - inputs.file(src) - outputs.dirs( - mapOf( - "psi" to psiDir, - "parser" to parserDir - ) - ) + this.grammarKit.setFrom(grammarKit) } } diff --git a/src/main/grammars/TranslationTemplateLexer.flex b/src/main/grammars/TranslationTemplateLexer.flex index b71ef2bc8..ba1831dcb 100644 --- a/src/main/grammars/TranslationTemplateLexer.flex +++ b/src/main/grammars/TranslationTemplateLexer.flex @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.translations.lang.gen; +package com.demonwav.mcdev.translations.template.gen; import com.intellij.lexer.*; import com.intellij.psi.tree.IElementType; diff --git a/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt b/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt index ad69540c4..b7413c898 100644 --- a/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt +++ b/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt @@ -20,7 +20,7 @@ package com.demonwav.mcdev.translations.sorting -import com.demonwav.mcdev.translations.lang.gen.TranslationTemplateLexer +import com.demonwav.mcdev.translations.template.gen.TranslationTemplateLexer import com.intellij.lexer.FlexAdapter class TranslationTemplateLexerAdapter : FlexAdapter(TranslationTemplateLexer()) From a3c6f09053af93bf3149fbdda7d91cb2df14c814 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 4 Jul 2024 16:08:58 +0200 Subject: [PATCH 05/37] Convert event generation UI to Kotlin DSL --- .../generation/ui/EventListenerWizard.form | 64 -------------- .../generation/ui/EventListenerWizard.kt | 88 ++++++++++--------- .../BukkitEventGenerationPanel.form | 48 ---------- .../generation/BukkitEventGenerationPanel.kt | 41 ++++----- .../BungeeCordEventGenerationPanel.form | 40 --------- .../BungeeCordEventGenerationPanel.kt | 29 +++--- .../SpongeEventGenerationPanel.form | 48 ---------- .../generation/SpongeEventGenerationPanel.kt | 42 +++++---- .../VelocityEventGenerationPanel.form | 40 --------- .../VelocityEventGenerationPanel.kt | 29 +++--- .../messages/MinecraftDevelopment.properties | 3 + 11 files changed, 117 insertions(+), 355 deletions(-) delete mode 100644 src/main/kotlin/insight/generation/ui/EventListenerWizard.form delete mode 100644 src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.form delete mode 100644 src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.form delete mode 100644 src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.form delete mode 100644 src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.form diff --git a/src/main/kotlin/insight/generation/ui/EventListenerWizard.form b/src/main/kotlin/insight/generation/ui/EventListenerWizard.form deleted file mode 100644 index c30595322..000000000 --- a/src/main/kotlin/insight/generation/ui/EventListenerWizard.form +++ /dev/null @@ -1,64 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt b/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt index 90daa78f1..1f8fabf67 100644 --- a/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt +++ b/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt @@ -22,60 +22,62 @@ package com.demonwav.mcdev.insight.generation.ui import com.intellij.ide.highlighter.JavaHighlightingColors import com.intellij.openapi.editor.ex.util.EditorUtil -import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy -import com.intellij.uiDesigner.core.GridConstraints +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.text import com.intellij.util.ui.UIUtil -import javax.swing.JLabel import javax.swing.JPanel -import javax.swing.JSeparator -import javax.swing.JTextField class EventListenerWizard(panel: JPanel?, className: String, defaultListenerName: String) { - lateinit var panel: JPanel - private lateinit var classNameTextField: JTextField - private lateinit var listenerNameTextField: JTextField - private lateinit var publicVoidLabel: JLabel - private lateinit var contentPanel: JPanel - private lateinit var separator: JSeparator - init { - classNameTextField.font = EditorUtil.getEditorFont() - listenerNameTextField.font = EditorUtil.getEditorFont() - publicVoidLabel.font = EditorUtil.getEditorFont() - if (UIUtil.isUnderDarcula()) { - publicVoidLabel.foreground = JavaHighlightingColors.KEYWORD.defaultAttributes.foregroundColor - } else { - publicVoidLabel.foreground = - JavaHighlightingColors.KEYWORD.fallbackAttributeKey!!.defaultAttributes.foregroundColor - } + private val graph = PropertyGraph("EventListenerWizard graph") - if (panel != null) { - separator.isVisible = true - contentPanel.add(panel, innerContentPanelConstraints) - } + private val listenerNameProperty = graph.property(defaultListenerName) + val chosenClassName: String by listenerNameProperty - classNameTextField.text = className - listenerNameTextField.text = defaultListenerName + val panel: JPanel by lazy { + panel { + row { + textField() + .text(className) + .align(AlignX.FILL) + .apply { + component.font = EditorUtil.getEditorFont() + component.isEditable = false + } + } - IdeFocusTraversalPolicy.getPreferredFocusedComponent(listenerNameTextField).requestFocus() - listenerNameTextField.requestFocus() - } + row { + label("public void").apply { + component.font = EditorUtil.getEditorFont() + if (UIUtil.isUnderDarcula()) { + component.foreground = JavaHighlightingColors.KEYWORD.defaultAttributes.foregroundColor + } else { + component.foreground = + JavaHighlightingColors.KEYWORD.fallbackAttributeKey!!.defaultAttributes.foregroundColor + } + } - val chosenClassName: String - get() = listenerNameTextField.text + textField() + .bindText(listenerNameProperty) + .columns(COLUMNS_LARGE) + .focused() + .apply { + component.font = EditorUtil.getEditorFont() + } + } - companion object { - private val innerContentPanelConstraints = GridConstraints() + if (panel != null) { + separator() - init { - innerContentPanelConstraints.row = 0 - innerContentPanelConstraints.column = 0 - innerContentPanelConstraints.rowSpan = 1 - innerContentPanelConstraints.colSpan = 1 - innerContentPanelConstraints.anchor = GridConstraints.ANCHOR_CENTER - innerContentPanelConstraints.fill = GridConstraints.FILL_BOTH - innerContentPanelConstraints.hSizePolicy = GridConstraints.SIZEPOLICY_FIXED - innerContentPanelConstraints.vSizePolicy = GridConstraints.SIZEPOLICY_FIXED + row { + cell(panel) + } + } } } } diff --git a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.form b/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.form deleted file mode 100644 index 9ae1aed1f..000000000 --- a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.form +++ /dev/null @@ -1,48 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt b/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt index 771e6750f..8a2230377 100644 --- a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt +++ b/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt @@ -20,40 +20,41 @@ package com.demonwav.mcdev.platform.bukkit.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.GenerationData import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JCheckBox -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class BukkitEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var ignoreCanceledCheckBox: JCheckBox - private lateinit var parentPanel: JPanel - private lateinit var eventPriorityComboBox: JComboBox + private val graph = PropertyGraph("BukkitEventGenerationPanel graph") - override val panel: JPanel - get() { - ignoreCanceledCheckBox.isSelected = true + private val ignoreCanceledProperty = graph.property(true) + private val eventPriorityProperty = graph.property("NORMAL") - // Not static because the form builder is not reliable - eventPriorityComboBox.addItem("MONITOR") - eventPriorityComboBox.addItem("HIGHEST") - eventPriorityComboBox.addItem("HIGH") - eventPriorityComboBox.addItem("NORMAL") - eventPriorityComboBox.addItem("LOW") - eventPriorityComboBox.addItem("LOWEST") + override val panel: JPanel by lazy { + panel { + row { + checkBox(MCDevBundle("generate.event_listener.ignore_if_canceled")) + .bindSelected(ignoreCanceledProperty) + } - eventPriorityComboBox.selectedIndex = 3 - - return parentPanel + row(MCDevBundle("generate.event_listener.event_priority")) { + comboBox(listOf("MONITOR", "HIGHEST", "HIGH", "NORMAL", "LOW", "LOWEST")) + .bindItem(eventPriorityProperty) + } } + } override fun gatherData(): GenerationData { return BukkitGenerationData( - ignoreCanceledCheckBox.isSelected, - eventPriorityComboBox.selectedItem?.toString() ?: error("No selected item") + ignoreCanceledProperty.get(), + eventPriorityProperty.get() ) } } diff --git a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.form b/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.form deleted file mode 100644 index 45a1c7c42..000000000 --- a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.form +++ /dev/null @@ -1,40 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt b/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt index d950005e7..b04f92156 100644 --- a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt +++ b/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt @@ -20,29 +20,28 @@ package com.demonwav.mcdev.platform.bungeecord.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class BungeeCordEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var eventPriorityComboBox: JComboBox - private lateinit var parentPanel: JPanel + private val graph = PropertyGraph("BungeeCordEventGenerationPanel graph") - override val panel: JPanel - get() { - // Not static because the form builder is not reliable - eventPriorityComboBox.addItem("HIGHEST") - eventPriorityComboBox.addItem("HIGH") - eventPriorityComboBox.addItem("NORMAL") - eventPriorityComboBox.addItem("LOW") - eventPriorityComboBox.addItem("LOWEST") + private val eventPriorityProperty = graph.property("NORMAL") - eventPriorityComboBox.selectedIndex = 2 - - return parentPanel + override val panel: JPanel by lazy { + panel { + row(MCDevBundle("generate.event_listener.event_priority")) { + comboBox(listOf("HIGHEST", "HIGH", "NORMAL", "LOW", "LOWEST")) + .bindItem(eventPriorityProperty) + } } + } - override fun gatherData() = BungeeCordGenerationData(eventPriorityComboBox.selectedItem.toString()) + override fun gatherData() = BungeeCordGenerationData(eventPriorityProperty.get()) } diff --git a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.form b/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.form deleted file mode 100644 index b49fc8dbd..000000000 --- a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.form +++ /dev/null @@ -1,48 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt b/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt index d02630f9a..b3796684c 100644 --- a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt +++ b/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt @@ -20,40 +20,38 @@ package com.demonwav.mcdev.platform.sponge.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.GenerationData import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JCheckBox -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class SpongeEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var parentPanel: JPanel - private lateinit var eventOrderComboBox: JComboBox - private lateinit var ignoreCanceledCheckBox: JCheckBox + private val graph = PropertyGraph("SpongeEventGenerationPanel graph") - override val panel: JPanel - get() { - ignoreCanceledCheckBox.isSelected = true + private val ignoreCanceledProperty = graph.property(true) + private val eventOrderProperty = graph.property("DEFAULT") - // Not static because the form builder is not reliable - eventOrderComboBox.addItem("PRE") - eventOrderComboBox.addItem("AFTER_PRE") - eventOrderComboBox.addItem("FIRST") - eventOrderComboBox.addItem("EARLY") - eventOrderComboBox.addItem("DEFAULT") - eventOrderComboBox.addItem("LATE") - eventOrderComboBox.addItem("LAST") - eventOrderComboBox.addItem("BEFORE_POST") - eventOrderComboBox.addItem("POST") + override val panel: JPanel by lazy { + panel { + row { + checkBox(MCDevBundle("generate.event_listener.ignore_if_canceled")) + .bindSelected(ignoreCanceledProperty) + } - eventOrderComboBox.selectedIndex = 4 - - return parentPanel + row(MCDevBundle("generate.event_listener.event_order")) { + comboBox(listOf("PRE", "AFTER_PRE", "FIRST", "EARLY", "DEFAULT", "LATE", "LAST", "BEFORE_POST", "POST")) + .bindItem(eventOrderProperty) + } } + } override fun gatherData(): GenerationData { - return SpongeGenerationData(ignoreCanceledCheckBox.isSelected, eventOrderComboBox.selectedItem as String) + return SpongeGenerationData(ignoreCanceledProperty.get(), eventOrderProperty.get()) } } diff --git a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.form b/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.form deleted file mode 100644 index d6653508a..000000000 --- a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.form +++ /dev/null @@ -1,40 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt b/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt index 503cd8ff5..f49225945 100644 --- a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt +++ b/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt @@ -20,32 +20,31 @@ package com.demonwav.mcdev.platform.velocity.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.GenerationData import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class VelocityEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var parentPanel: JPanel - private lateinit var eventOrderComboBox: JComboBox + private val graph = PropertyGraph("VelocityEventGenerationPanel graph") - override val panel: JPanel - get() { - // Not static because the form builder is not reliable - eventOrderComboBox.addItem("FIRST") - eventOrderComboBox.addItem("EARLY") - eventOrderComboBox.addItem("NORMAL") - eventOrderComboBox.addItem("LATE") - eventOrderComboBox.addItem("LAST") + private val eventOrderProperty = graph.property("NORMAL") - eventOrderComboBox.selectedIndex = 2 - - return parentPanel + override val panel: JPanel by lazy { + panel { + row(MCDevBundle("generate.event_listener.event_order")) { + comboBox(listOf("FIRST", "EARLY", "NORMAL", "LATE", "LAST")) + .bindItem(eventOrderProperty) + } } + } override fun gatherData(): GenerationData { - return VelocityGenerationData(eventOrderComboBox.selectedItem as String) + return VelocityGenerationData(eventOrderProperty.get()) } } diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index a71c4539f..21e09cdfc 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -98,6 +98,9 @@ facet.reimport.failed.content.with_error=Failed to start project refresh, please generate.event_listener.title=Generate Event Listener generate.event_listener.settings=Event Listener Settings +generate.event_listener.event_priority=Event Priority +generate.event_listener.event_order=Event Order +generate.event_listener.ignore_if_canceled=Ignore if event is canceled generate.class.caption=Minecraft Class generate.class.description=Class generation for modders From 6aa9273da52cc76483f324f922eff23d2475d272 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 4 Jul 2024 16:09:10 +0200 Subject: [PATCH 06/37] Only show "Generate Accessor/Invoke" in Mixin modules --- .../kotlin/platform/mixin/action/GenerateAccessorAction.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt b/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt index 865eb6296..921b260ce 100644 --- a/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt +++ b/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.action +import com.demonwav.mcdev.platform.mixin.MixinModuleType import com.intellij.codeInsight.FileModificationService import com.intellij.codeInsight.generation.actions.BaseGenerateAction import com.intellij.openapi.application.ApplicationManager @@ -75,7 +76,7 @@ class GenerateAccessorAction : BaseGenerateAction(GenerateAccessorHandler()) { } override fun isValidForFile(project: Project, editor: Editor, file: PsiFile): Boolean { - if (file !is PsiJavaFile) { + if (file !is PsiJavaFile || !MixinModuleType.isInModule(file)) { return false } From ca003f90104183b54a82500a44edfe62bcc43082 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 4 Jul 2024 18:49:09 +0200 Subject: [PATCH 07/37] Convert last forms to Kotlin UI DSL --- .../mixin/action/FindMixinsComponent.form | 26 ------ .../mixin/action/FindMixinsComponent.kt | 21 +++-- .../actions/TranslationSortOrderDialog.form | 89 ------------------- .../actions/TranslationSortOrderDialog.kt | 72 +++++++++------ .../TranslationTemplateConfigurable.form | 58 ------------ .../TranslationTemplateConfigurable.kt | 53 +++++++---- .../messages/MinecraftDevelopment.properties | 10 +++ 7 files changed, 99 insertions(+), 230 deletions(-) delete mode 100644 src/main/kotlin/platform/mixin/action/FindMixinsComponent.form delete mode 100644 src/main/kotlin/translations/actions/TranslationSortOrderDialog.form delete mode 100644 src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.form diff --git a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.form b/src/main/kotlin/platform/mixin/action/FindMixinsComponent.form deleted file mode 100644 index 8b9f602e7..000000000 --- a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.form +++ /dev/null @@ -1,26 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt b/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt index bf06d7f6e..b0504e6e8 100644 --- a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt +++ b/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt @@ -20,27 +20,26 @@ package com.demonwav.mcdev.platform.mixin.action -import com.demonwav.mcdev.util.toArray import com.intellij.ide.util.PsiClassListCellRenderer import com.intellij.psi.PsiClass import com.intellij.ui.components.JBList +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JPanel -import javax.swing.ListModel class FindMixinsComponent(classes: List) : MouseAdapter() { - private lateinit var classList: JBList - lateinit var panel: JPanel - private set - - init { - @Suppress("UNCHECKED_CAST") - classList.model = JBList.createDefaultListModel(*classes.toArray()) as ListModel - classList.cellRenderer = PsiClassListCellRenderer() + private val classList = JBList(classes).apply { + cellRenderer = PsiClassListCellRenderer() + addMouseListener(this@FindMixinsComponent) + } - classList.addMouseListener(this) + val panel: JPanel = panel { + row { + cell(classList).align(Align.FILL) + } } override fun mouseClicked(e: MouseEvent) { diff --git a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.form b/src/main/kotlin/translations/actions/TranslationSortOrderDialog.form deleted file mode 100644 index 477020b84..000000000 --- a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.form +++ /dev/null @@ -1,89 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt b/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt index 2c284a5f5..c72b6817f 100644 --- a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt +++ b/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt @@ -20,44 +20,58 @@ package com.demonwav.mcdev.translations.actions +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.translations.sorting.Ordering +import com.intellij.CommonBundle +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.bindIntValue +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.panel import java.awt.Component import java.awt.event.KeyEvent import java.awt.event.WindowAdapter import java.awt.event.WindowEvent -import javax.swing.DefaultComboBoxModel import javax.swing.DefaultListCellRenderer -import javax.swing.JButton -import javax.swing.JComboBox import javax.swing.JComponent import javax.swing.JDialog import javax.swing.JList -import javax.swing.JPanel -import javax.swing.JSpinner import javax.swing.KeyStroke -import javax.swing.SpinnerNumberModel import javax.swing.WindowConstants class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection: Ordering) : JDialog() { - private lateinit var contentPane: JPanel - private lateinit var buttonOK: JButton - private lateinit var buttonCancel: JButton - private lateinit var comboSelection: JComboBox - private lateinit var spinnerComments: JSpinner - init { - setContentPane(contentPane) - isModal = true - title = "Select Sort Order" - getRootPane().defaultButton = buttonOK + private val graph = PropertyGraph("TranslationSortOrderDialog graph") + + private val orderProperty = graph.property(defaultSelection) + private val keepCommentsProperty = graph.property(0) - buttonOK.addActionListener { onOK() } - buttonCancel.addActionListener { onCancel() } - spinnerComments.model = SpinnerNumberModel(0, 0, Int.MAX_VALUE, 1) + private var canceled = false + + init { val availableOrderings = if (excludeDefaultOption) NON_DEFAULT_ORDERINGS else ALL_ORDERINGS - comboSelection.model = DefaultComboBoxModel(availableOrderings) - comboSelection.renderer = CellRenderer - comboSelection.selectedItem = defaultSelection + val panel = panel { + row(MCDevBundle("translation_sort.order")) { + comboBox(availableOrderings, CellRenderer) + .bindItem(orderProperty) + } + + row(MCDevBundle("translation_sort.keep_comment")) { + spinner(0..Int.MAX_VALUE) + .bindIntValue(keepCommentsProperty::get, keepCommentsProperty::set) + } + + row { + button(CommonBundle.message("button.ok")) { onOK() }.align(AlignX.RIGHT).also { + getRootPane().defaultButton = it.component + } + button(CommonBundle.message("button.cancel")) { onCancel() }.align(AlignX.RIGHT) + } + } + contentPane = panel + + isModal = true + title = MCDevBundle("translation_sort.title") // call onCancel() when cross is clicked defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE @@ -70,7 +84,7 @@ class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection ) // call onCancel() on ESCAPE - contentPane.registerKeyboardAction( + panel.registerKeyboardAction( { onCancel() }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, @@ -82,7 +96,7 @@ class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection } private fun onCancel() { - comboSelection.selectedIndex = -1 + canceled = true dispose() } @@ -100,17 +114,17 @@ class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection } companion object { - private val ALL_ORDERINGS = Ordering.values() - private val NON_DEFAULT_ORDERINGS = Ordering.values() - .filterNot { it == Ordering.LIKE_DEFAULT }.toTypedArray() + private val ALL_ORDERINGS = Ordering.entries + private val NON_DEFAULT_ORDERINGS = Ordering.entries + .filterNot { it == Ordering.LIKE_DEFAULT } fun show(excludeDefaultOption: Boolean, defaultSelection: Ordering): Pair { val dialog = TranslationSortOrderDialog(excludeDefaultOption, defaultSelection) dialog.pack() dialog.setLocationRelativeTo(dialog.owner) dialog.isVisible = true - val order = dialog.comboSelection.selectedItem as? Ordering - val comments = dialog.spinnerComments.value as Int + val order = if (dialog.canceled) null else dialog.orderProperty.get() + val comments = dialog.keepCommentsProperty.get() return (order to comments) } } diff --git a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.form b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.form deleted file mode 100644 index 267a814de..000000000 --- a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.form +++ /dev/null @@ -1,58 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt index 87c9200c2..e4e148a47 100644 --- a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt +++ b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.translations.sorting +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.translations.lang.colors.LangSyntaxHighlighter import com.intellij.codeInsight.template.impl.TemplateEditorUtil import com.intellij.ide.DataManager @@ -30,6 +31,8 @@ import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel import com.intellij.util.ui.JBUI import java.awt.BorderLayout import javax.swing.DefaultComboBoxModel @@ -39,32 +42,47 @@ import javax.swing.JPanel import org.jetbrains.annotations.Nls class TranslationTemplateConfigurable(private val project: Project) : Configurable { - private lateinit var panel: JPanel private lateinit var cmbScheme: JComboBox - private lateinit var editorPanel: JPanel - private lateinit var templateEditor: Editor + private var templateEditor: Editor? = null + + private val editorPanel = JPanel(BorderLayout()).apply { + preferredSize = JBUI.size(250, 450) + minimumSize = preferredSize + } + + private val panel = panel { + row(MCDevBundle("minecraft.settings.lang_template.scheme")) { + cmbScheme = comboBox(emptyList()).component + } + + row { + label(MCDevBundle("minecraft.settings.lang_template.comment")) + } + + row { + cell(editorPanel).align(Align.FILL) + } + } @Nls - override fun getDisplayName() = "Localization Template" + override fun getDisplayName() = MCDevBundle("minecraft.settings.lang_template.display_name") override fun getHelpTopic(): String? = null - override fun createComponent(): JComponent { - return panel - } + override fun createComponent(): JComponent = panel private fun getActiveTemplateText() = when { cmbScheme.selectedIndex == 0 -> TemplateManager.getGlobalTemplateText() !project.isDefault -> TemplateManager.getProjectTemplateText(project) - else -> "You must have selected a project for this!" + else -> MCDevBundle("minecraft.settings.lang_template.project_must_be_selected") } private fun init() { if (project.isDefault) { - cmbScheme.selectedIndex = 0 cmbScheme.model = DefaultComboBoxModel(arrayOf("Global")) - } else if (cmbScheme.selectedIndex == 0) { + cmbScheme.selectedIndex = 0 + } else { cmbScheme.model = DefaultComboBoxModel(arrayOf("Global", "Project")) } cmbScheme.addActionListener { @@ -82,24 +100,25 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab editorColorsScheme, ) (templateEditor as EditorEx).highlighter = highlighter - templateEditor.settings.isLineNumbersShown = true + templateEditor!!.settings.isLineNumbersShown = true - editorPanel.preferredSize = JBUI.size(250, 100) - editorPanel.minimumSize = editorPanel.preferredSize editorPanel.removeAll() - editorPanel.add(templateEditor.component, BorderLayout.CENTER) + editorPanel.add(templateEditor!!.component, BorderLayout.CENTER) } override fun isModified(): Boolean { - return templateEditor.document.text != getActiveTemplateText() + return templateEditor?.document?.text != getActiveTemplateText() != false } override fun apply() { + val editor = templateEditor + ?: return + val project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(panel)) if (cmbScheme.selectedIndex == 0) { - TemplateManager.writeGlobalTemplate(templateEditor.document.text) + TemplateManager.writeGlobalTemplate(editor.document.text) } else if (project != null) { - TemplateManager.writeProjectTemplate(project, templateEditor.document.text) + TemplateManager.writeProjectTemplate(project, editor.document.text) } } diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 21e09cdfc..c21cade20 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -193,6 +193,10 @@ nbt.file.save_notify.parse_exception.content=An unexpected exception happened, { intention.error.cannot.create.class.message=Cannot create class ''{0}''\n{1} intention.error.cannot.create.class.title=Failed to Create Class +translation_sort.title=Select Sort Order +translation_sort.order=Sort Order +translation_sort.keep_comment=Keep Comment + minecraft.settings.display_name=Minecraft Development minecraft.settings.title=Minecraft Development Settings minecraft.settings.change_update_channel=Change Plugin Update Channel @@ -203,3 +207,9 @@ minecraft.settings.show_chat_color_underlines=Show chat color underlines minecraft.settings.chat_color_underline_style=Chat color underline style: minecraft.settings.mixin=Mixin minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line +minecraft.settings.lang_template.display_name=Localization Template +minecraft.settings.lang_template.scheme=Scheme: +minecraft.settings.lang_template.project_must_be_selected=You must have selected a project for this! +minecraft.settings.lang_template.comment=You may edit the template used for translation key sorting here.\ +
Each line may be empty, a comment (with #) or a glob pattern for matching translation keys (like "item.*").\ +
Note: Empty lines are respected and will be put into the sorting result. From f1a1e0ad3ff95ea185f0b32cd2805b23574cc058 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 4 Jul 2024 20:32:39 +0200 Subject: [PATCH 08/37] Disable code instrumentation We should not need it anymore now that the last forms are gone That should shorten the compilation time by a bit! --- build.gradle.kts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 3b2663f07..ecb6e12c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -398,3 +398,11 @@ tasks.buildSearchableOptions { // not working atm enabled = false } + +tasks.instrumentCode { + enabled = false +} + +tasks.instrumentedJar { + enabled = false +} From 322cf27619bae830fcab97607dd6a1a2074cf67a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 4 Jul 2024 20:34:43 +0200 Subject: [PATCH 09/37] Fix build It didn't get included in the previous commit --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index ecb6e12c7..a47d000e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -377,6 +377,7 @@ tasks.register("cleanSandbox", Delete::class) { } tasks.withType { + pluginJar.set(tasks.jar.get().archiveFile) from(externalAnnotationsJar) { into("Minecraft Development/lib/resources") } From 838067d05f6eaa1521fcbc16f6c7ba4022401f4a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Fri, 5 Jul 2024 10:44:56 +0200 Subject: [PATCH 10/37] Fix #2310 Translations aren't detected for enum constructors --- .../kotlin/translations/identification/TranslationIdentifier.kt | 2 +- src/main/kotlin/util/call-utils.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/translations/identification/TranslationIdentifier.kt b/src/main/kotlin/translations/identification/TranslationIdentifier.kt index 2b74bfe1d..f5be4bd04 100644 --- a/src/main/kotlin/translations/identification/TranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/TranslationIdentifier.kt @@ -69,7 +69,7 @@ abstract class TranslationIdentifier { if (container !is PsiExpressionList) { return null } - val call = container.parent as? PsiCallExpression ?: return null + val call = container.parent as? PsiCall ?: return null val index = container.expressions.indexOf(element) val method = call.referencedMethod ?: return null diff --git a/src/main/kotlin/util/call-utils.kt b/src/main/kotlin/util/call-utils.kt index 8e45a5a3c..90cb1ac6d 100644 --- a/src/main/kotlin/util/call-utils.kt +++ b/src/main/kotlin/util/call-utils.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.util import com.intellij.psi.PsiCall +import com.intellij.psi.PsiEnumConstant import com.intellij.psi.PsiExpression import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression @@ -32,6 +33,7 @@ val PsiCall.referencedMethod: PsiMethod? get() = when (this) { is PsiMethodCallExpression -> this.methodExpression.advancedResolve(false).element as PsiMethod? is PsiNewExpression -> this.resolveMethod() + is PsiEnumConstant -> this.resolveMethod() else -> null } From 8f06f6002549f2f6e1f828ec5fc7102a5259b4f3 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Fri, 5 Jul 2024 10:57:51 +0200 Subject: [PATCH 11/37] Fix #2260 expects varargs as return value for varargs target --- ...InvalidInjectorMethodSignatureInspection.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 5d636de7f..a51fd6db3 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -34,6 +34,7 @@ import com.demonwav.mcdev.platform.mixin.util.isConstructor import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.fullQualifiedName +import com.demonwav.mcdev.util.normalize import com.demonwav.mcdev.util.synchronize import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview import com.intellij.codeInsight.intention.QuickFixFactory @@ -46,6 +47,7 @@ import com.intellij.psi.JavaElementVisitor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifier import com.intellij.psi.PsiNameHelper @@ -55,7 +57,6 @@ import com.intellij.psi.PsiType import com.intellij.psi.codeStyle.JavaCodeStyleManager import com.intellij.psi.codeStyle.VariableKind import com.intellij.psi.util.PsiUtil -import com.intellij.psi.util.TypeConversionUtil import org.objectweb.asm.Opcodes class InvalidInjectorMethodSignatureInspection : MixinInspection() { @@ -195,13 +196,18 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { ) { reportedSignature = true + val normalizedExpected = when (expectedReturnType) { + is PsiEllipsisType -> expectedReturnType.toArrayType() + else -> expectedReturnType + } + holder.registerProblem( method.returnTypeElement ?: identifier, - "Expected return type '${expectedReturnType.presentableText}' " + + "Expected return type '${normalizedExpected.presentableText}' " + "for $annotationName method", QuickFixFactory.getInstance().createMethodReturnFix( method, - expectedReturnType, + normalizedExpected, false, ), ) @@ -218,9 +224,9 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { method: PsiMethod, allowCoerce: Boolean, ): Boolean { - val expectedErasure = TypeConversionUtil.erasure(expectedReturnType) - val returnErasure = TypeConversionUtil.erasure(methodReturnType) - if (expectedErasure == returnErasure) { + val normalizedExpected = expectedReturnType.normalize() + val normalizedReturn = methodReturnType.normalize() + if (normalizedExpected == normalizedReturn) { return true } if (!allowCoerce || !method.hasAnnotation(COERCE)) { From f798d6d3544971f546cb1d13231edc021b74bf89 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Fri, 5 Jul 2024 12:11:06 +0200 Subject: [PATCH 12/37] Remove unused import --- .../kotlin/translations/identification/TranslationIdentifier.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/translations/identification/TranslationIdentifier.kt b/src/main/kotlin/translations/identification/TranslationIdentifier.kt index f5be4bd04..fc5b5a0b1 100644 --- a/src/main/kotlin/translations/identification/TranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/TranslationIdentifier.kt @@ -38,7 +38,6 @@ import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiCall -import com.intellij.psi.PsiCallExpression import com.intellij.psi.PsiElement import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiExpression From caca374c623d9f3315f3c9f9fc3e40597c40b28f Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 6 Jul 2024 11:37:38 +0200 Subject: [PATCH 13/37] Fix #2325 Make lang annotator fixes bulk compatible --- .../intentions/RemoveDuplicatesIntention.kt | 22 ++++++++++----- .../RemoveUnmatchedEntryIntention.kt | 19 +++++++------ .../intentions/TranslationFileAnnotator.kt | 6 ++--- .../intentions/TrimKeyIntention.kt | 27 +++++++++++++------ 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt b/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt index 52d4dcac5..30f227b40 100644 --- a/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt +++ b/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt @@ -23,24 +23,32 @@ package com.demonwav.mcdev.translations.intentions import com.demonwav.mcdev.translations.Translation import com.demonwav.mcdev.translations.TranslationFiles import com.demonwav.mcdev.translations.index.TranslationInverseIndex -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope -class RemoveDuplicatesIntention(private val translation: Translation) : PsiElementBaseIntentionAction() { +class RemoveDuplicatesIntention( + private val translation: Translation, + element: PsiElement +) : LocalQuickFixAndIntentionActionOnPsiElement(element) { override fun getText() = "Remove duplicates (keep this translation)" override fun getFamilyName() = "Minecraft localization" - override fun isAvailable(project: Project, editor: Editor?, element: PsiElement) = true - - override fun invoke(project: Project, editor: Editor?, element: PsiElement) { - val keep = TranslationFiles.seekTranslation(element) ?: return + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + val keep = TranslationFiles.seekTranslation(startElement) ?: return val entries = TranslationInverseIndex.findElements( translation.key, - GlobalSearchScope.fileScope(element.containingFile), + GlobalSearchScope.fileScope(file), ) for (other in entries) { if (other !== keep) { diff --git a/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt b/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt index 6185d6f2c..78884869e 100644 --- a/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt +++ b/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt @@ -21,21 +21,24 @@ package com.demonwav.mcdev.translations.intentions import com.demonwav.mcdev.translations.TranslationFiles -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement -import com.intellij.util.IncorrectOperationException +import com.intellij.psi.PsiFile -class RemoveUnmatchedEntryIntention : PsiElementBaseIntentionAction() { +class RemoveUnmatchedEntryIntention(element: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(element) { override fun getText() = "Remove translation" - override fun isAvailable(project: Project, editor: Editor, element: PsiElement) = true - override fun getFamilyName() = "Minecraft" - @Throws(IncorrectOperationException::class) - override fun invoke(project: Project, editor: Editor, element: PsiElement) { - TranslationFiles.remove(TranslationFiles.seekTranslation(element) ?: return) + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + TranslationFiles.remove(TranslationFiles.seekTranslation(startElement) ?: return) } } diff --git a/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt b/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt index 535d3fe6e..302384d1d 100644 --- a/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt +++ b/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt @@ -49,7 +49,7 @@ class TranslationFileAnnotator : Annotator { if (translation.key != translation.trimmedKey) { annotations.newAnnotation(HighlightSeverity.WARNING, "Translation key contains whitespace at start or end.") .range(element) - .newFix(TrimKeyIntention()).registerFix() + .newFix(TrimKeyIntention(element)).universal().registerFix() .create() } } @@ -58,7 +58,7 @@ class TranslationFileAnnotator : Annotator { val count = TranslationIndex.getTranslations(element.containingFile).count { it.key == translation.key } if (count > 1) { annotations.newAnnotation(HighlightSeverity.WARNING, "Duplicate translation keys \"${translation.key}\".") - .newFix(RemoveDuplicatesIntention(translation)).registerFix() + .newFix(RemoveDuplicatesIntention(translation, element)).universal().registerFix() .create() } } @@ -71,7 +71,7 @@ class TranslationFileAnnotator : Annotator { } val warningText = "Translation key not included in default localization file." annotations.newAnnotation(HighlightSeverity.WARNING, warningText) - .newFix(RemoveUnmatchedEntryIntention()).registerFix() + .newFix(RemoveUnmatchedEntryIntention(element)).universal().registerFix() .create() } } diff --git a/src/main/kotlin/translations/intentions/TrimKeyIntention.kt b/src/main/kotlin/translations/intentions/TrimKeyIntention.kt index 0e4a9851b..9e532db28 100644 --- a/src/main/kotlin/translations/intentions/TrimKeyIntention.kt +++ b/src/main/kotlin/translations/intentions/TrimKeyIntention.kt @@ -22,28 +22,39 @@ package com.demonwav.mcdev.translations.intentions import com.demonwav.mcdev.translations.TranslationFiles import com.intellij.codeInsight.FileModificationService -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement -import com.intellij.util.IncorrectOperationException +import com.intellij.psi.PsiFile -class TrimKeyIntention : PsiElementBaseIntentionAction() { +class TrimKeyIntention(element: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(element) { override fun getText() = "Trim translation key" override fun getFamilyName() = "Minecraft" - override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean { + override fun isAvailable( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ): Boolean { val translation = TranslationFiles.toTranslation( - TranslationFiles.seekTranslation(element) ?: return false, + TranslationFiles.seekTranslation(startElement) ?: return false, ) ?: return false return translation.key != translation.trimmedKey } - @Throws(IncorrectOperationException::class) - override fun invoke(project: Project, editor: Editor, element: PsiElement) { - val entry = TranslationFiles.seekTranslation(element) ?: return + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + val entry = TranslationFiles.seekTranslation(startElement) ?: return if (!FileModificationService.getInstance().preparePsiElementForWrite(entry)) { return } From 336924bed50a7b31ab01929da3477c5742cf9b02 Mon Sep 17 00:00:00 2001 From: Moulberry Date: Fri, 3 May 2024 17:26:57 +0800 Subject: [PATCH 14/37] Translation: option to force json and configurable default i18n call --- src/main/kotlin/TranslationSettings.kt | 71 +++++++++++++++++++ .../mcp/mappings/HardcodedYarnToMojmap.kt | 9 +++ .../kotlin/platform/mcp/mappings/Mappings.kt | 7 ++ .../kotlin/translations/TranslationFiles.kt | 54 ++++++++++++-- .../ConvertToTranslationIntention.kt | 24 ++++++- .../TranslationTemplateConfigurable.kt | 33 ++++++++- src/main/resources/META-INF/plugin.xml | 1 + .../messages/MinecraftDevelopment.properties | 3 + 8 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/TranslationSettings.kt diff --git a/src/main/kotlin/TranslationSettings.kt b/src/main/kotlin/TranslationSettings.kt new file mode 100644 index 000000000..f2b5cf483 --- /dev/null +++ b/src/main/kotlin/TranslationSettings.kt @@ -0,0 +1,71 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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 + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project + +@State(name = "TranslationSettings", storages = [Storage("minecraft_dev.xml")]) +class TranslationSettings : PersistentStateComponent { + + data class State( + var isForceJsonTranslationFile: Boolean = false, + var isUseCustomConvertToTranslationTemplate: Boolean = false, + var convertToTranslationTemplate: String = "net.minecraft.client.resources.I18n.format(\"\$key\")", + ) + + private var state = State() + + override fun getState(): State { + return state + } + + override fun loadState(state: State) { + this.state = state + } + + // State mappings + var isForceJsonTranslationFile: Boolean + get() = state.isForceJsonTranslationFile + set(forceJsonTranslationFile) { + state.isForceJsonTranslationFile = forceJsonTranslationFile + } + + var isUseCustomConvertToTranslationTemplate: Boolean + get() = state.isUseCustomConvertToTranslationTemplate + set(useCustomConvertToTranslationTemplate) { + state.isUseCustomConvertToTranslationTemplate = useCustomConvertToTranslationTemplate + } + + var convertToTranslationTemplate: String + get() = state.convertToTranslationTemplate + set(convertToTranslationTemplate) { + state.convertToTranslationTemplate = convertToTranslationTemplate + } + + companion object { + @JvmStatic + fun getInstance(project: Project): TranslationSettings = project.service() + } +} diff --git a/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt b/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt index 5f3182872..e2919e8e0 100644 --- a/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt +++ b/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt @@ -43,6 +43,15 @@ object HardcodedYarnToMojmap { owner = "net.minecraft.network.chat.Component", name = "translatableEscape", descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" + ), + MemberReference( + owner = "net.minecraft.client.resource.language.I18n", + name = "translate", + descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;" + ) mapTo MemberReference( + owner = "net.minecraft.client.resources.language.I18n", + name = "get", + descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;" ) ), hashMapOf(), diff --git a/src/main/kotlin/platform/mcp/mappings/Mappings.kt b/src/main/kotlin/platform/mcp/mappings/Mappings.kt index 9ddf8e6a6..1135d40e9 100644 --- a/src/main/kotlin/platform/mcp/mappings/Mappings.kt +++ b/src/main/kotlin/platform/mcp/mappings/Mappings.kt @@ -119,6 +119,13 @@ fun Module.getMappedMethod(mojangClass: String, mojangMethod: String, mojangDesc return getMappedMethod(MemberReference(mojangMethod, mojangDescriptor, mojangClass)) } +fun Module.getMappedMethodCall(mojangClass: String, mojangMethod: String, mojangDescriptor: String, p: String): String { + val mappedMethodRef = namedToMojang?.tryGetMappedMethod( + MemberReference(mojangMethod, mojangDescriptor, mojangClass) + ) ?: return "$mojangClass.$mojangMethod($p)" + return "${mappedMethodRef.owner}.${mappedMethodRef.name}($p)" +} + fun Module.getMojangMethod(mappedMethod: MemberReference): String { return namedToMojang?.getIntermediaryMethod(mappedMethod)?.name ?: return mappedMethod.name } diff --git a/src/main/kotlin/translations/TranslationFiles.kt b/src/main/kotlin/translations/TranslationFiles.kt index b2fd47f71..8b8f74027 100644 --- a/src/main/kotlin/translations/TranslationFiles.kt +++ b/src/main/kotlin/translations/TranslationFiles.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.translations +import com.demonwav.mcdev.TranslationSettings import com.demonwav.mcdev.translations.index.TranslationIndex import com.demonwav.mcdev.translations.index.TranslationInverseIndex import com.demonwav.mcdev.translations.lang.LangFile @@ -110,12 +111,43 @@ object TranslationFiles { element.delete() } + fun findTranslationKeyForText(context: PsiElement, text: String): String? { + val module = context.findModule() + ?: throw IllegalArgumentException("Cannot add translation for element outside of module") + var jsonVersion = true + if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) { + val version = + context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") + jsonVersion = version > MC_1_12_2 + } + + if (!jsonVersion) { + // This feature only supports JSON translation files + return null + } + + val files = FileTypeIndex.getFiles( + JsonFileType.INSTANCE, + GlobalSearchScope.moduleScope(module), + ).filter { getLocale(it) == TranslationConstants.DEFAULT_LOCALE } + + for (file in files) { + val psiFile = PsiManager.getInstance(context.project).findFile(file) ?: continue + psiFile.findKeyForTextAsJson(text)?.let { return it } + } + + return null + } + fun add(context: PsiElement, key: String, text: String) { val module = context.findModule() ?: throw IllegalArgumentException("Cannot add translation for element outside of module") - val version = - context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") - val jsonVersion = version > MC_1_12_2 + var jsonVersion = true + if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) { + val version = + context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") + jsonVersion = version > MC_1_12_2 + } fun write(files: Iterable) { for (file in files) { @@ -223,6 +255,13 @@ object TranslationFiles { doc.insertString(rootObject.lastChild.prevSibling.textOffset, content) } + private fun PsiFile.findKeyForTextAsJson(text: String): String? { + val rootObject = this.firstChild as? JsonObject ?: return null + return rootObject.propertyList.firstOrNull { + (it.value as? JsonStringLiteral)?.value == text + }?.name + } + private fun generateJsonFile( leadingComma: Boolean, indent: CharSequence, @@ -292,9 +331,12 @@ object TranslationFiles { fun buildSortingTemplateFromDefault(context: PsiElement, domain: String? = null): Template? { val module = context.findModule() ?: throw IllegalArgumentException("Cannot add translation for element outside of module") - val version = - context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") - val jsonVersion = version > MC_1_12_2 + var jsonVersion = true + if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) { + val version = + context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") + jsonVersion = version > MC_1_12_2 + } val defaultTranslationFile = FileBasedIndex.getInstance() .getContainingFiles( diff --git a/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt b/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt index aa3046dc5..9db73187d 100644 --- a/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt +++ b/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt @@ -20,7 +20,10 @@ package com.demonwav.mcdev.translations.intentions +import com.demonwav.mcdev.TranslationSettings +import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethodCall import com.demonwav.mcdev.translations.TranslationFiles +import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.runWriteAction import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction import com.intellij.lang.java.JavaLanguage @@ -42,6 +45,9 @@ class ConvertToTranslationIntention : PsiElementBaseIntentionAction() { override fun invoke(project: Project, editor: Editor, element: PsiElement) { if (element.parent is PsiLiteral) { val value = (element.parent as PsiLiteral).value as? String ?: return + + val existingKey = TranslationFiles.findTranslationKeyForText(element, value) + val result = Messages.showInputDialogWithCheckBox( "Enter translation key:", "Convert String Literal to Translation", @@ -49,7 +55,7 @@ class ConvertToTranslationIntention : PsiElementBaseIntentionAction() { true, true, Messages.getQuestionIcon(), - null, + existingKey, object : InputValidatorEx { override fun getErrorText(inputString: String): String? { if (inputString.isEmpty()) { @@ -73,12 +79,24 @@ class ConvertToTranslationIntention : PsiElementBaseIntentionAction() { val key = result.first ?: return val replaceLiteral = result.second try { - TranslationFiles.add(element, key, value) + if (existingKey != key) { + TranslationFiles.add(element, key, value) + } if (replaceLiteral) { + val translationSettings = TranslationSettings.getInstance(project) val psi = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return psi.runWriteAction { val expression = JavaPsiFacade.getElementFactory(project).createExpressionFromText( - "net.minecraft.client.resources.I18n.format(\"$key\")", + if (translationSettings.isUseCustomConvertToTranslationTemplate) { + translationSettings.convertToTranslationTemplate.replace("\$key", key) + } else { + element.findModule()?.getMappedMethodCall( + "net.minecraft.client.resource.language.I18n", + "translate", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", + "\"$key\"" + ) ?: "net.minecraft.client.resource.I18n.get(\"$key\")" + }, element.context, ) if (psi.language === JavaLanguage.INSTANCE) { diff --git a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt index e4e148a47..20daa77ee 100644 --- a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt +++ b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.translations.sorting +import com.demonwav.mcdev.TranslationSettings import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.translations.lang.colors.LangSyntaxHighlighter import com.intellij.codeInsight.template.impl.TemplateEditorUtil @@ -32,7 +33,13 @@ import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.selected +import com.intellij.ui.layout.ComponentPredicate import com.intellij.util.ui.JBUI import java.awt.BorderLayout import javax.swing.DefaultComboBoxModel @@ -46,7 +53,7 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab private var templateEditor: Editor? = null private val editorPanel = JPanel(BorderLayout()).apply { - preferredSize = JBUI.size(250, 450) + preferredSize = JBUI.size(250, 350) minimumSize = preferredSize } @@ -62,6 +69,25 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab row { cell(editorPanel).align(Align.FILL) } + + val translationSettings = TranslationSettings.getInstance(project) + row { + checkBox(MCDevBundle("minecraft.settings.translation.force_json_translation_file")) + .bindSelected(translationSettings::isForceJsonTranslationFile) + } + + lateinit var allowConvertToTranslationTemplate: ComponentPredicate + row { + val checkBox = checkBox(MCDevBundle("minecraft.settings.translation.use_custom_convert_template")) + .bindSelected(translationSettings::isUseCustomConvertToTranslationTemplate) + allowConvertToTranslationTemplate = checkBox.selected + } + + row { + textField().bindText(translationSettings::convertToTranslationTemplate) + .enabledIf(allowConvertToTranslationTemplate) + .columns(COLUMNS_LARGE) + } } @Nls @@ -107,7 +133,7 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab } override fun isModified(): Boolean { - return templateEditor?.document?.text != getActiveTemplateText() != false + return templateEditor?.document?.text != getActiveTemplateText() != false || panel.isModified() } override fun apply() { @@ -120,9 +146,12 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab } else if (project != null) { TemplateManager.writeProjectTemplate(project, editor.document.text) } + + panel.apply() } override fun reset() { init() + panel.reset() } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 4f18c81e1..5be203414 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -285,6 +285,7 @@ + diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index c21cade20..7ab481b10 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -213,3 +213,6 @@ minecraft.settings.lang_template.project_must_be_selected=You must have selected minecraft.settings.lang_template.comment=You may edit the template used for translation key sorting here.\
Each line may be empty, a comment (with #) or a glob pattern for matching translation keys (like "item.*").\
Note: Empty lines are respected and will be put into the sorting result. +minecraft.settings.translation=Translation +minecraft.settings.translation.force_json_translation_file=Force JSON translation file (1.13+) +minecraft.settings.translation.use_custom_convert_template=Use custom template for convert literal to translation From fbbb2a2162bdf2a56241a81d8b2b317c11063232 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 7 Jul 2024 17:52:45 +0200 Subject: [PATCH 15/37] Provide Minecraft version in Loom-based projects --- .../FabricLoomModelBuilderImpl.groovy | 3 ++- .../fabricloom/FabricLoomModelImpl.groovy | 1 + .../tooling/fabricloom/FabricLoomModel.java | 2 ++ .../FabricLoomProjectResolverExtension.kt | 20 +++++++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy index 9cfd8f225..c751e8998 100644 --- a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy @@ -50,6 +50,7 @@ class FabricLoomModelBuilderImpl implements ModelBuilderService { } FabricLoomModel build(Project project, Object loomExtension) { + def minecraftVersion = loomExtension.minecraftProvider.minecraftVersion() def tinyMappings = loomExtension.mappingsFile def splitMinecraftJar = loomExtension.areEnvironmentSourceSetsSplit() @@ -70,7 +71,7 @@ class FabricLoomModelBuilderImpl implements ModelBuilderService { } //noinspection GroovyAssignabilityCheck - return new FabricLoomModelImpl(tinyMappings, decompilers, splitMinecraftJar, modSourceSets) + return new FabricLoomModelImpl(minecraftVersion, tinyMappings, decompilers, splitMinecraftJar, modSourceSets) } List getDecompilers(Object loomExtension, boolean client) { diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy index 1a04fab1e..d54149dbf 100644 --- a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy @@ -24,6 +24,7 @@ import groovy.transform.Immutable @Immutable(knownImmutableClasses = [File]) class FabricLoomModelImpl implements FabricLoomModel, Serializable { + String minecraftVersion File tinyMappings Map> decompilers boolean splitMinecraftJar diff --git a/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java index 6198402bf..e984864a2 100644 --- a/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java +++ b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java @@ -26,6 +26,8 @@ public interface FabricLoomModel { + String getMinecraftVersion(); + File getTinyMappings(); Map> getDecompilers(); diff --git a/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt b/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt index 82ec68855..b89478dcc 100644 --- a/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt +++ b/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt @@ -20,10 +20,13 @@ package com.demonwav.mcdev.platform.mcp.fabricloom +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.gradle.McpModelData import com.demonwav.mcdev.platform.mcp.gradle.tooling.fabricloom.FabricLoomModel import com.intellij.openapi.externalSystem.model.DataNode import com.intellij.openapi.externalSystem.model.project.ModuleData import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension class FabricLoomProjectResolverExtension : AbstractProjectResolverExtension() { @@ -50,6 +53,23 @@ class FabricLoomProjectResolverExtension : AbstractProjectResolverExtension() { loomData.modSourceSets ) ideModule.createChild(FabricLoomData.KEY, data) + + val mcpData = McpModelData( + ideModule.data, + McpModuleSettings.State( + minecraftVersion = loomData.minecraftVersion, + ), + null, + null + ) + ideModule.createChild(McpModelData.KEY, mcpData) + + for (child in ideModule.children) { + val childData = child.data + if (childData is GradleSourceSetData) { + child.createChild(McpModelData.KEY, mcpData.copy(module = childData)) + } + } } super.populateModuleExtraModels(gradleModule, ideModule) From 3a2175f12ed5a6843df98363bce0c011e3e1b1c0 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 10 Jul 2024 13:34:04 +0200 Subject: [PATCH 16/37] Translation: initial conversion to UAST --- src/main/kotlin/translations/folding.kt | 33 +++++-- .../LiteralTranslationIdentifier.kt | 16 +-- .../ReferenceTranslationIdentifier.kt | 34 +++---- .../identification/TranslationIdentifier.kt | 59 ++++++----- .../identification/TranslationInstance.kt | 8 +- .../inspections/ChangeTranslationQuickFix.kt | 28 +++--- .../inspections/MissingFormatInspection.kt | 28 +++--- .../inspections/NoTranslationInspection.kt | 24 +++-- .../SuperfluousFormatInspection.kt | 56 ++++++----- .../inspections/TranslationInspection.kt | 4 +- .../WrongTypeInTranslationArgsInspection.kt | 99 ++++++++++++------- .../translations/reference/contributors.kt | 46 ++++----- src/main/kotlin/util/call-uast-utils.kt | 72 ++++++++++++++ src/main/kotlin/util/call-utils.kt | 41 -------- src/main/kotlin/util/expression-utils.kt | 40 ++++---- src/main/resources/META-INF/plugin.xml | 12 +-- 16 files changed, 351 insertions(+), 249 deletions(-) create mode 100644 src/main/kotlin/util/call-uast-utils.kt diff --git a/src/main/kotlin/translations/folding.kt b/src/main/kotlin/translations/folding.kt index b58fe7b41..f71f95651 100644 --- a/src/main/kotlin/translations/folding.kt +++ b/src/main/kotlin/translations/folding.kt @@ -34,8 +34,11 @@ import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.FoldingGroup import com.intellij.openapi.options.BeanConfigurable import com.intellij.psi.PsiElement -import com.intellij.psi.PsiExpressionList -import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.textRange +import org.jetbrains.uast.toUElement +import org.jetbrains.uast.visitor.AbstractUastVisitor class TranslationCodeFoldingOptionsProvider : BeanConfigurable(TranslationFoldingSettings.instance), CodeFoldingOptionsProvider { @@ -88,23 +91,35 @@ class TranslationFoldingBuilder : FoldingBuilderEx() { val descriptors = mutableListOf() for (identifier in TranslationIdentifier.INSTANCES) { - val elements = PsiTreeUtil.findChildrenOfType(root, identifier.elementClass()) - for (element in elements) { + val uElement = root.toUElement() ?: continue + val children = mutableListOf() + uElement.accept(object : AbstractUastVisitor() { + override fun visitElement(node: UElement): Boolean { + if (identifier.elementClass().isAssignableFrom(node.javaClass)) { + children.add(node) + } + + return super.visitElement(node) + } + }) + for (element in children) { val translation = identifier.identifyUnsafe(element) val foldingElement = translation?.foldingElement ?: continue val range = - if (foldingElement is PsiExpressionList) { - val args = foldingElement.expressions.drop(translation.foldStart) - args.first().textRange.union(args.last().textRange) + if (foldingElement is UCallExpression && translation.foldStart != 0) { + val args = foldingElement.valueArguments.drop(translation.foldStart) + val startRange = args.first().textRange ?: continue + val endRange = args.last().textRange ?: continue + startRange.union(endRange) } else { - foldingElement.textRange + foldingElement.textRange ?: continue } if (!translation.required && translation.formattingError != null) { continue } descriptors.add( FoldingDescriptor( - translation.foldingElement.node, + translation.foldingElement.sourcePsi?.node!!, range, FoldingGroup.newGroup("mc.translation." + translation.key), if (translation.formattingError == TranslationInstance.Companion.FormattingError.MISSING) { diff --git a/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt b/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt index a8806ab42..befdb4630 100644 --- a/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt @@ -21,13 +21,15 @@ package com.demonwav.mcdev.translations.identification import com.intellij.codeInsight.completion.CompletionUtilCore -import com.intellij.psi.PsiLiteralExpression +import org.jetbrains.uast.ULiteralExpression -class LiteralTranslationIdentifier : TranslationIdentifier() { - override fun identify(element: PsiLiteralExpression): TranslationInstance? { - val statement = element.parent - if (element.value is String) { - val result = identify(element.project, element, statement, element) +class LiteralTranslationIdentifier : TranslationIdentifier() { + override fun identify(element: ULiteralExpression): TranslationInstance? { + val statement = element.uastParent + if (statement != null && element.value is String) { + val project = element.sourcePsi?.project + ?: return null + val result = identify(project, element, statement, element) return result?.copy( key = result.key.copy( infix = result.key.infix.replace( @@ -40,5 +42,5 @@ class LiteralTranslationIdentifier : TranslationIdentifier return null } - override fun elementClass(): Class = PsiLiteralExpression::class.java + override fun elementClass(): Class = ULiteralExpression::class.java } diff --git a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt index b6085615b..99fc8c3bf 100644 --- a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt @@ -22,29 +22,29 @@ package com.demonwav.mcdev.translations.identification import com.intellij.codeInsight.completion.CompletionUtilCore import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiField -import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiModifier -import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.impl.source.PsiClassReferenceType import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.uast.UField +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.resolveToUElement -class ReferenceTranslationIdentifier : TranslationIdentifier() { - override fun identify(element: PsiReferenceExpression): TranslationInstance? { - val reference = element.resolve() - val statement = element.parent +class ReferenceTranslationIdentifier : TranslationIdentifier() { + override fun identify(element: UReferenceExpression): TranslationInstance? { + val reference = element.resolveToUElement() ?: return null + val statement = element.uastParent ?: return null + val project = element.sourcePsi?.project ?: return null - if (reference is PsiField) { - val scope = GlobalSearchScope.allScope(element.project) + if (reference is UField) { + val scope = GlobalSearchScope.allScope(project) val stringClass = - JavaPsiFacade.getInstance(element.project).findClass("java.lang.String", scope) ?: return null - val isConstant = - reference.hasModifierProperty(PsiModifier.STATIC) && reference.hasModifierProperty(PsiModifier.FINAL) + JavaPsiFacade.getInstance(project).findClass("java.lang.String", scope) ?: return null + val isConstant = reference.isStatic && reference.isFinal val type = reference.type as? PsiClassReferenceType ?: return null val resolved = type.resolve() ?: return null if (isConstant && (resolved.isEquivalentTo(stringClass) || resolved.isInheritor(stringClass, true))) { - val referenceElement = reference.initializer as? PsiLiteral ?: return null - val result = identify(element.project, element, statement, referenceElement) + val referenceElement = reference.uastInitializer as? ULiteralExpression ?: return null + val result = identify(project, element, statement, referenceElement) return result?.copy( key = result.key.copy( @@ -60,7 +60,5 @@ class ReferenceTranslationIdentifier : TranslationIdentifier { - return PsiReferenceExpression::class.java - } + override fun elementClass(): Class = UReferenceExpression::class.java } diff --git a/src/main/kotlin/translations/identification/TranslationIdentifier.kt b/src/main/kotlin/translations/identification/TranslationIdentifier.kt index fc5b5a0b1..766a8886e 100644 --- a/src/main/kotlin/translations/identification/TranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/TranslationIdentifier.kt @@ -37,18 +37,22 @@ import com.intellij.codeInspection.dataFlow.CommonDataflow import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiCall import com.intellij.psi.PsiElement import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiExpressionList -import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter import java.util.IllegalFormatException import java.util.MissingFormatArgumentException - -abstract class TranslationIdentifier { +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.getContainingUClass + +abstract class TranslationIdentifier { @Suppress("UNCHECKED_CAST") - fun identifyUnsafe(element: PsiElement): TranslationInstance? { + fun identifyUnsafe(element: UElement): TranslationInstance? { return identify(element as T) } @@ -61,20 +65,19 @@ abstract class TranslationIdentifier { fun identify( project: Project, - element: PsiExpression, - container: PsiElement, - referenceElement: PsiElement, + element: UExpression, + container: UElement, + referenceElement: UElement, ): TranslationInstance? { - if (container !is PsiExpressionList) { - return null - } - val call = container.parent as? PsiCall ?: return null - val index = container.expressions.indexOf(element) + val call = container as? UCallExpression ?: return null + val index = container.valueArguments.indexOf(element) val method = call.referencedMethod ?: return null - val parameter = method.parameterList.getParameter(index) ?: return null - val translatableAnnotation = - AnnotationUtil.findAnnotation(parameter, TranslationConstants.TRANSLATABLE_ANNOTATION) ?: return null + val parameter = method.uastParameters.getOrNull(index) ?: return null + val translatableAnnotation = AnnotationUtil.findAnnotation( + parameter.javaPsi as PsiParameter, + TranslationConstants.TRANSLATABLE_ANNOTATION + ) ?: return null val prefix = translatableAnnotation.findAttributeValue(TranslationConstants.PREFIX)?.constantStringValue ?: "" @@ -84,13 +87,16 @@ abstract class TranslationIdentifier { translatableAnnotation.findAttributeValue(TranslationConstants.REQUIRED)?.constantValue as? Boolean ?: true val isPreEscapeException = - method.containingClass?.qualifiedName?.startsWith("net.minecraft.") == true && - isPreEscapeMcVersion(project, element) + method.getContainingUClass()?.qualifiedName?.startsWith("net.minecraft.") == true && + isPreEscapeMcVersion(project, element.sourcePsi!!) val allowArbitraryArgs = isPreEscapeException || translatableAnnotation.findAttributeValue( TranslationConstants.ALLOW_ARBITRARY_ARGS )?.constantValue as? Boolean ?: false - val translationKey = CommonDataflow.computeValue(element) as? String ?: return null + val translationKey = when (val javaPsi = element.javaPsi) { + is PsiExpression -> CommonDataflow.computeValue(javaPsi) as? String + else -> element.evaluateString() + } ?: return null val entries = TranslationIndex.getAllDefaultEntries(project).merge("") val translation = entries[prefix + translationKey + suffix]?.text @@ -109,15 +115,15 @@ abstract class TranslationIdentifier { ?: false val formatting = - (method.parameterList.parameters.last().type as? PsiEllipsisType) + (method.uastParameters.last().type as? PsiEllipsisType) ?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) == true val foldingElement = if (foldMethod) { call } else if ( index == 0 && - container.expressionCount > 1 && - method.parameterList.parametersCount == 2 && + container.valueArgumentCount > 1 && + method.uastParameters.size == 2 && formatting ) { container @@ -155,14 +161,15 @@ abstract class TranslationIdentifier { } } - private fun format(method: PsiMethod, translation: String, call: PsiCall): Pair? { + private fun format(method: UMethod, translation: String, call: UCallExpression): Pair? { val format = NUMBER_FORMATTING_PATTERN.replace(translation, "%$1s") val paramCount = STRING_FORMATTING_PATTERN.findAll(format).count() - val varargs = call.extractVarArgs(method.parameterList.parametersCount - 1, true, true) + val parametersCount = method.uastParameters.size + val varargs = call.extractVarArgs(parametersCount - 1, true, true) ?: return null val varargStart = if (varargs.size > paramCount) { - method.parameterList.parametersCount - 1 + paramCount + parametersCount - 1 + paramCount } else { -1 } diff --git a/src/main/kotlin/translations/identification/TranslationInstance.kt b/src/main/kotlin/translations/identification/TranslationInstance.kt index 9976e9c85..393836cbe 100644 --- a/src/main/kotlin/translations/identification/TranslationInstance.kt +++ b/src/main/kotlin/translations/identification/TranslationInstance.kt @@ -20,12 +20,12 @@ package com.demonwav.mcdev.translations.identification -import com.intellij.psi.PsiElement +import org.jetbrains.uast.UElement data class TranslationInstance( - val foldingElement: PsiElement?, + val foldingElement: UElement?, val foldStart: Int, - val referenceElement: PsiElement?, + val referenceElement: UElement?, val key: Key, val text: String?, val required: Boolean, @@ -44,7 +44,7 @@ data class TranslationInstance( MISSING, SUPERFLUOUS } - fun find(element: PsiElement): TranslationInstance? = + fun find(element: UElement): TranslationInstance? = TranslationIdentifier.INSTANCES .firstOrNull { it.elementClass().isAssignableFrom(element.javaClass) } ?.identifyUnsafe(element) diff --git a/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt b/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt index 69891966d..b05ac801b 100644 --- a/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt +++ b/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt @@ -29,17 +29,19 @@ import com.intellij.ide.util.gotoByName.ChooseByNamePopup import com.intellij.ide.util.gotoByName.ChooseByNamePopupComponent import com.intellij.openapi.application.ModalityState import com.intellij.openapi.project.Project -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiNamedElement import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.generate.getUastElementFactory +import org.jetbrains.uast.generate.replace +import org.jetbrains.uast.toUElementOfType class ChangeTranslationQuickFix(private val name: String) : LocalQuickFix { override fun getName() = name override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { - val literal = descriptor.psiElement as PsiLiteralExpression + val literal = descriptor.psiElement.toUElementOfType() ?: return val key = LiteralTranslationIdentifier().identify(literal)?.key ?: return val popup = ChooseByNamePopup.createPopup( project, @@ -50,17 +52,15 @@ class ChangeTranslationQuickFix(private val name: String) : LocalQuickFix { object : ChooseByNamePopupComponent.Callback() { override fun elementChosen(element: Any) { val selectedKey = (element as PsiNamedElement).name ?: return - literal.containingFile.runWriteAction { - val insertion = selectedKey.substring( - key.prefix.length, - selectedKey.length - key.suffix.length, - ) - literal.replace( - JavaPsiFacade.getInstance(project).elementFactory.createExpressionFromText( - "\"$insertion\"", - literal.context, - ), - ) + val insertion = selectedKey.substring( + key.prefix.length, + selectedKey.length - key.suffix.length, + ) + val elementFactory = literal.getUastElementFactory(project) ?: return + val replacement = elementFactory.createStringLiteralExpression(insertion, element) + ?: return + descriptor.psiElement.containingFile.runWriteAction { + literal.replace(replacement) } } }, diff --git a/src/main/kotlin/translations/inspections/MissingFormatInspection.kt b/src/main/kotlin/translations/inspections/MissingFormatInspection.kt index 89e66b265..65eb0b74f 100644 --- a/src/main/kotlin/translations/inspections/MissingFormatInspection.kt +++ b/src/main/kotlin/translations/inspections/MissingFormatInspection.kt @@ -24,31 +24,35 @@ import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.identification.TranslationInstance.Companion.FormattingError import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemsHolder -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiReferenceExpression +import com.intellij.uast.UastHintedVisitorAdapter +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class MissingFormatInspection : TranslationInspection() { override fun getStaticDescription() = "Detects missing format arguments for translations" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), arrayOf(UExpression::class.java)) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - visit(expression) + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitExpression(node: UExpression): Boolean { + visit(node) + return super.visitElement(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - visit(expression, ChangeTranslationQuickFix("Use a different translation")) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + visit(node, ChangeTranslationQuickFix("Use a different translation")) + return true } - private fun visit(expression: PsiExpression, vararg quickFixes: LocalQuickFix) { + private fun visit(expression: UExpression, vararg quickFixes: LocalQuickFix) { val result = TranslationInstance.find(expression) if (result != null && result.required && result.formattingError == FormattingError.MISSING) { holder.registerProblem( - expression, + expression.sourcePsi!!, "There are missing formatting arguments to satisfy '${result.text}'", *quickFixes, ) diff --git a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt index e588155f4..d1f931636 100644 --- a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt +++ b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt @@ -29,29 +29,35 @@ import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiLiteralExpression +import com.intellij.uast.UastHintedVisitorAdapter import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.toUElementOfType +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class NoTranslationInspection : TranslationInspection() { override fun getStaticDescription() = "Checks whether a translation key used in calls to StatCollector.translateToLocal(), " + "StatCollector.translateToLocalFormatted() or I18n.format() exists." - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), arrayOf(ULiteralExpression::class.java)) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - val result = LiteralTranslationIdentifier().identify(expression) + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + val result = LiteralTranslationIdentifier().identify(node) if (result != null && result.required && result.text == null) { holder.registerProblem( - expression, + node.sourcePsi!!, "The given translation key does not exist", CreateTranslationQuickFix, ChangeTranslationQuickFix("Use existing translation"), ) } + + return true } } @@ -60,7 +66,7 @@ class NoTranslationInspection : TranslationInspection() { override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { - val literal = descriptor.psiElement as PsiLiteralExpression + val literal = descriptor.psiElement.toUElementOfType() ?: return val translation = LiteralTranslationIdentifier().identify(literal) val literalValue = literal.value as String val key = translation?.key?.copy(infix = literalValue)?.full ?: literalValue @@ -71,7 +77,7 @@ class NoTranslationInspection : TranslationInspection() { Messages.getQuestionIcon(), ) if (result != null) { - TranslationFiles.add(literal, key, result) + TranslationFiles.add(literal.sourcePsi!!, key, result) } } catch (ignored: IncorrectOperationException) { } catch (e: Exception) { diff --git a/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt b/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt index b6b9496c3..a3448dcc0 100644 --- a/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt +++ b/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt @@ -27,58 +27,68 @@ import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project -import com.intellij.psi.JavaElementVisitor -import com.intellij.psi.PsiCall import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.SmartPointerManager -import com.intellij.psi.SmartPsiElementPointer +import com.intellij.uast.UastHintedVisitorAdapter +import com.intellij.uast.UastSmartPointer +import com.intellij.uast.createUastSmartPointer import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class SuperfluousFormatInspection : TranslationInspection() { override fun getStaticDescription() = "Detect superfluous format arguments for translations" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + private val typesHint: Array> = + arrayOf(UReferenceExpression::class.java, ULiteralExpression::class.java) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - val result = TranslationInstance.find(expression) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitExpression(node: UExpression): Boolean { + val result = TranslationInstance.find(node) if ( - result != null && result.foldingElement is PsiCall && + result != null && result.foldingElement is UCallExpression && result.formattingError == FormattingError.SUPERFLUOUS ) { - registerProblem(expression, result) + registerProblem(node, result) } + + return super.visitExpression(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - val result = TranslationInstance.find(expression) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + val result = TranslationInstance.find(node) if ( - result != null && result.required && result.foldingElement is PsiCall && + result != null && result.required && result.foldingElement is UCallExpression && result.formattingError == FormattingError.SUPERFLUOUS ) { registerProblem( - expression, + node, result, RemoveArgumentsQuickFix( - SmartPointerManager.getInstance(holder.project) - .createSmartPsiElementPointer(result.foldingElement), + result.foldingElement.createUastSmartPointer(), result.superfluousVarargStart, ), ChangeTranslationQuickFix("Use a different translation"), ) } + + return super.visitLiteralExpression(node) } private fun registerProblem( - expression: PsiExpression, + expression: UExpression, result: TranslationInstance, vararg quickFixes: LocalQuickFix, ) { holder.registerProblem( - expression, + expression.sourcePsi!!, "There are missing formatting arguments to satisfy '${result.text}'", *quickFixes, ) @@ -86,7 +96,7 @@ class SuperfluousFormatInspection : TranslationInspection() { } private class RemoveArgumentsQuickFix( - private val call: SmartPsiElementPointer, + private val call: UastSmartPointer, private val position: Int, ) : LocalQuickFix { override fun getName() = "Remove superfluous arguments" @@ -94,7 +104,7 @@ class SuperfluousFormatInspection : TranslationInspection() { override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { descriptor.psiElement.containingFile.runWriteAction { - call.element?.argumentList?.expressions?.drop(position)?.forEach { it.delete() } + call.element?.valueArguments?.drop(position)?.forEach { it.sourcePsi?.delete() } } } catch (ignored: IncorrectOperationException) { } diff --git a/src/main/kotlin/translations/inspections/TranslationInspection.kt b/src/main/kotlin/translations/inspections/TranslationInspection.kt index bfb3c0664..1c3852d46 100644 --- a/src/main/kotlin/translations/inspections/TranslationInspection.kt +++ b/src/main/kotlin/translations/inspections/TranslationInspection.kt @@ -21,14 +21,14 @@ package com.demonwav.mcdev.translations.inspections import com.demonwav.mcdev.platform.mcp.McpModuleType -import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFile -abstract class TranslationInspection : AbstractBaseJavaLocalInspectionTool() { +abstract class TranslationInspection : LocalInspectionTool() { protected abstract fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor final override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { diff --git a/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt b/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt index 40b30620a..370ddf2c9 100644 --- a/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt +++ b/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt @@ -24,59 +24,84 @@ import com.demonwav.mcdev.platform.mcp.mappings.getMappedClass import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethod import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.intention.FileModifier import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiCall import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiFile -import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiManager -import com.intellij.psi.PsiMethodCallExpression -import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiType +import com.intellij.uast.UastHintedVisitorAdapter +import com.intellij.uast.createUastSmartPointer import com.siyeh.ig.psiutils.CommentTracker import com.siyeh.ig.psiutils.MethodCallUtils +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UIdentifier +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.generate.replace +import org.jetbrains.uast.getContainingUClass +import org.jetbrains.uast.resolveToUElement +import org.jetbrains.uast.util.isMethodCall +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class WrongTypeInTranslationArgsInspection : TranslationInspection() { override fun getStaticDescription() = "Detect wrong argument types in translation arguments" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - doCheck(expression) + private val typesHint: Array> = + arrayOf(UReferenceExpression::class.java, ULiteralExpression::class.java) + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitElement(node: UElement): Boolean { + if (node is UReferenceExpression) { + doCheck(node) + } + + return super.visitElement(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - doCheck(expression) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + doCheck(node) + return super.visitLiteralExpression(node) } - private fun doCheck(element: PsiElement) { + private fun doCheck(element: UElement) { val result = TranslationInstance.find(element) - if (result == null || result.foldingElement !is PsiCall || result.allowArbitraryArgs) { + if (result == null || result.foldingElement !is UCallExpression || result.allowArbitraryArgs) { return } - val args = result.foldingElement.argumentList ?: return + val args = result.foldingElement.valueArguments - if (!MethodCallUtils.isVarArgCall(result.foldingElement)) { + val javaCall = result.foldingElement.javaPsi as? PsiCall ?: return + if (!MethodCallUtils.isVarArgCall(javaCall)) { return } - val resolvedMethod = result.foldingElement.resolveMethod() ?: return - if ((resolvedMethod.parameterList.parameters.lastOrNull()?.type as? PsiEllipsisType) + val resolvedMethod = result.foldingElement.resolveToUElement() as? UMethod ?: return + val parameters = resolvedMethod.uastParameters + if ((parameters.lastOrNull()?.type as? PsiEllipsisType) ?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) != true ) { return } - val module = element.findModule() ?: return + val elementSourcePsi = element.sourcePsi ?: return + val module = elementSourcePsi.findModule() ?: return val componentName = module.getMappedClass("net.minecraft.network.chat.Component") val translatableName = module.getMappedMethod( "net.minecraft.network.chat.Component", @@ -84,30 +109,31 @@ class WrongTypeInTranslationArgsInspection : TranslationInspection() { "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" ) val isComponentTranslatable = resolvedMethod.name == translatableName && - resolvedMethod.containingClass?.qualifiedName == componentName + resolvedMethod.getContainingUClass()?.qualifiedName == componentName + val resolveScope = elementSourcePsi.resolveScope val booleanType = - PsiType.getTypeByName(CommonClassNames.JAVA_LANG_BOOLEAN, holder.project, element.resolveScope) + PsiType.getTypeByName(CommonClassNames.JAVA_LANG_BOOLEAN, holder.project, resolveScope) val numberType = - PsiType.getTypeByName(CommonClassNames.JAVA_LANG_NUMBER, holder.project, element.resolveScope) - val stringType = PsiType.getJavaLangString(PsiManager.getInstance(holder.project), element.resolveScope) - val componentType = PsiType.getTypeByName(componentName, holder.project, element.resolveScope) - for (arg in args.expressions.drop(resolvedMethod.parameterList.parametersCount - 1)) { - val type = arg.type ?: continue + PsiType.getTypeByName(CommonClassNames.JAVA_LANG_NUMBER, holder.project, resolveScope) + val stringType = PsiType.getJavaLangString(PsiManager.getInstance(holder.project), resolveScope) + val componentType = PsiType.getTypeByName(componentName, holder.project, resolveScope) + for (arg in args.drop(parameters.size - 1)) { + val type = arg.getExpressionType() ?: continue if (!booleanType.isAssignableFrom(type) && !numberType.isAssignableFrom(type) && !stringType.isAssignableFrom(type) && !componentType.isAssignableFrom(type) ) { - var fixes = arrayOf(WrapWithStringValueOfFix(arg)) - if (isComponentTranslatable && result.foldingElement is PsiMethodCallExpression) { - val referenceName = result.foldingElement.methodExpression.referenceNameElement + var fixes = arrayOf(WrapWithStringValueOfFix(arg.sourcePsi!!)) + if (isComponentTranslatable && result.foldingElement.isMethodCall()) { + val referenceName = result.foldingElement.methodIdentifier if (referenceName != null) { fixes = arrayOf(ReplaceWithTranslatableEscapedFix(referenceName)) + fixes } } holder.registerProblem( - arg, + arg.sourcePsi!!, "Translation argument is not a 'String', 'Number', 'Boolean' or 'Component'", *fixes ) @@ -117,19 +143,24 @@ class WrongTypeInTranslationArgsInspection : TranslationInspection() { } private class ReplaceWithTranslatableEscapedFix( - referenceName: PsiElement - ) : LocalQuickFixOnPsiElement(referenceName) { + identifier: UIdentifier + ) : LocalQuickFix { + + @FileModifier.SafeFieldForPreview + private val identifierPointer = identifier.createUastSmartPointer() + override fun getFamilyName() = "Replace with 'Component.translatableEscaped'" - override fun getText() = "Replace with 'Component.translatableEscaped'" - override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { - val module = startElement.findModule() ?: return + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val identifier = identifierPointer.element ?: return + val module = identifier.sourcePsi!!.findModule() ?: return val newMethodName = module.getMappedMethod( "net.minecraft.network.chat.Component", "translatableEscape", "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" ) - startElement.replace(JavaPsiFacade.getElementFactory(project).createIdentifier(newMethodName)) + val fakeSourcePsi = JavaPsiFacade.getElementFactory(project).createIdentifier(newMethodName) + identifier.replace(UIdentifier(fakeSourcePsi, identifier.uastParent)) } } diff --git a/src/main/kotlin/translations/reference/contributors.kt b/src/main/kotlin/translations/reference/contributors.kt index 0ea7f0f68..5ba569a8e 100644 --- a/src/main/kotlin/translations/reference/contributors.kt +++ b/src/main/kotlin/translations/reference/contributors.kt @@ -21,7 +21,6 @@ package com.demonwav.mcdev.translations.reference import com.demonwav.mcdev.translations.TranslationFiles -import com.demonwav.mcdev.translations.identification.TranslationIdentifier import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.lang.gen.psi.LangEntry import com.demonwav.mcdev.translations.lang.gen.psi.LangTypes @@ -34,34 +33,29 @@ import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.PsiReferenceRegistrar +import com.intellij.psi.registerUastReferenceProvider +import com.intellij.psi.uastReferenceProvider import com.intellij.util.ProcessingContext +import org.jetbrains.uast.UElement -class JavaReferenceContributor : PsiReferenceContributor() { +class UastReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { - for (identifier in TranslationIdentifier.INSTANCES) { - registrar.registerReferenceProvider( - PlatformPatterns.psiElement(identifier.elementClass()), - object : PsiReferenceProvider() { - override fun getReferencesByElement( - element: PsiElement, - context: ProcessingContext, - ): Array { - val result = identifier.identifyUnsafe(element) - if (result != null) { - val referenceElement = result.referenceElement ?: return emptyArray() - return arrayOf( - TranslationReference( - referenceElement, - TextRange(1, referenceElement.textLength - 1), - result.key, - ), - ) - } - return emptyArray() - } - }, - ) - } + registrar.registerUastReferenceProvider( + { _, _ -> true }, + uastReferenceProvider { uExpr, psi -> + val translation = TranslationInstance.find(uExpr) + ?: return@uastReferenceProvider emptyArray() + val referenceElement = translation.referenceElement + ?: return@uastReferenceProvider emptyArray() + arrayOf( + TranslationReference( + psi, + TextRange(1, referenceElement.asSourceString().length - 1), + translation.key, + ), + ) + } + ) } } diff --git a/src/main/kotlin/util/call-uast-utils.kt b/src/main/kotlin/util/call-uast-utils.kt new file mode 100644 index 000000000..af29f8029 --- /dev/null +++ b/src/main/kotlin/util/call-uast-utils.kt @@ -0,0 +1,72 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.util + +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.toUElementOfType +import org.jetbrains.uast.util.isArrayInitializer + +val UCallExpression.referencedMethod: UMethod? + get() = this.resolve()?.toUElementOfType() + +fun UCallExpression.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array? { + val method = this.referencedMethod + val args = this.valueArguments + if (method == null || args.size < (index + 1)) { + return emptyArray() + } + + val psiParam = method.uastParameters[index].javaPsi as? PsiParameter + ?: return null + if (!psiParam.isVarArgs) { + return arrayOf(args[index].evaluate(allowTranslations, allowReferences)) + } + + val elements = args.drop(index) + return extractVarArgs(psiParam.type, elements, allowReferences, allowTranslations) +} + +private fun extractVarArgs( + type: PsiType, + elements: List, + allowReferences: Boolean, + allowTranslations: Boolean, +): Array? { + return if (elements[0].getExpressionType() == type) { + val initializer = elements[0] + if (initializer is UCallExpression && initializer.isArrayInitializer()) { + // We're dealing with an array initializer, let's analyse it! + initializer.valueArguments + .asSequence() + .map { it.evaluate(allowReferences, allowTranslations) } + .toTypedArray() + } else { + // We're dealing with a more complex expression that results in an array, give up + return null + } + } else { + elements.asSequence().map { it.evaluate(allowReferences, allowTranslations) }.toTypedArray() + } +} diff --git a/src/main/kotlin/util/call-utils.kt b/src/main/kotlin/util/call-utils.kt index 90cb1ac6d..672a03ce5 100644 --- a/src/main/kotlin/util/call-utils.kt +++ b/src/main/kotlin/util/call-utils.kt @@ -22,12 +22,9 @@ package com.demonwav.mcdev.util import com.intellij.psi.PsiCall import com.intellij.psi.PsiEnumConstant -import com.intellij.psi.PsiExpression import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiNewExpression -import com.intellij.psi.PsiSubstitutor -import com.intellij.psi.PsiType val PsiCall.referencedMethod: PsiMethod? get() = when (this) { @@ -36,41 +33,3 @@ val PsiCall.referencedMethod: PsiMethod? is PsiEnumConstant -> this.resolveMethod() else -> null } - -fun PsiCall.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array? { - val method = this.referencedMethod - val args = this.argumentList?.expressions ?: return emptyArray() - if (method == null || args.size < (index + 1)) { - return emptyArray() - } - if (!method.parameterList.parameters[index].isVarArgs) { - return arrayOf(args[index].evaluate(allowTranslations, allowReferences)) - } - - val varargType = method.getSignature(PsiSubstitutor.EMPTY).parameterTypes[index] - val elements = args.drop(index) - return extractVarArgs(varargType, elements, allowReferences, allowTranslations) -} - -private fun extractVarArgs( - type: PsiType, - elements: List, - allowReferences: Boolean, - allowTranslations: Boolean, -): Array? { - return if (elements[0].type == type) { - val initializer = elements[0] - if (initializer is PsiNewExpression && initializer.arrayInitializer != null) { - // We're dealing with an array initializer, let's analyse it! - initializer.arrayInitializer!!.initializers - .asSequence() - .map { it.evaluate(allowReferences, allowTranslations) } - .toTypedArray() - } else { - // We're dealing with a more complex expression that results in an array, give up - return null - } - } else { - elements.asSequence().map { it.evaluate(allowReferences, allowTranslations) }.toTypedArray() - } -} diff --git a/src/main/kotlin/util/expression-utils.kt b/src/main/kotlin/util/expression-utils.kt index 736547c1e..ad39742b3 100644 --- a/src/main/kotlin/util/expression-utils.kt +++ b/src/main/kotlin/util/expression-utils.kt @@ -22,34 +22,38 @@ package com.demonwav.mcdev.util import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.identification.TranslationInstance.Companion.FormattingError -import com.intellij.psi.PsiAnnotationMemberValue -import com.intellij.psi.PsiCall -import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.PsiTypeCastExpression -import com.intellij.psi.PsiVariable +import org.jetbrains.uast.UBinaryExpressionWithType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.UVariable +import org.jetbrains.uast.util.isTypeCast -fun PsiAnnotationMemberValue.evaluate(allowReferences: Boolean, allowTranslations: Boolean): String? { - val visited = mutableSetOf() +fun UExpression.evaluate(allowReferences: Boolean, allowTranslations: Boolean): String? { + val visited = mutableSetOf() - fun eval(expr: PsiAnnotationMemberValue?, defaultValue: String? = null): String? { + fun eval(expr: UExpression?, defaultValue: String? = null): String? { if (!visited.add(expr)) { return defaultValue } when { - expr is PsiTypeCastExpression && expr.operand != null -> + expr is UBinaryExpressionWithType && expr.isTypeCast() -> return eval(expr.operand, defaultValue) - expr is PsiReferenceExpression -> { - val reference = expr.advancedResolve(false).element - if (reference is PsiVariable && reference.initializer != null) { - return eval(reference.initializer, "\${${expr.text}}") + + expr is UReferenceExpression -> { + val reference = expr.resolve() + if (reference is UVariable && reference.uastInitializer != null) { + return eval(reference.uastInitializer, "\${${expr.asSourceString()}}") } } - expr is PsiLiteral -> + + expr is ULiteralExpression -> return expr.value.toString() - expr is PsiCall && allowTranslations -> - for (argument in expr.argumentList?.expressions ?: emptyArray()) { + + expr is UCallExpression && allowTranslations -> + for (argument in expr.valueArguments) { val translation = TranslationInstance.find(argument) ?: continue if (translation.formattingError == FormattingError.MISSING) { return "{ERROR: Missing formatting arguments for '${translation.text}'}" @@ -60,7 +64,7 @@ fun PsiAnnotationMemberValue.evaluate(allowReferences: Boolean, allowTranslation } return if (allowReferences && expr != null) { - "\${${expr.text}}" + "\${${expr.asSourceString()}}" } else { defaultValue } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 5be203414..e8dae1cc0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -249,11 +249,11 @@ - + - + @@ -517,28 +517,28 @@ implementationClass="com.demonwav.mcdev.inspection.IsCancelledInspection"/> Date: Wed, 10 Jul 2024 14:25:08 +0200 Subject: [PATCH 17/37] Translation: TranslationIdentifier instances cleanup Also support instance final fields for reference identifiers --- .../LiteralTranslationIdentifier.kt | 22 ++++------ .../ReferenceTranslationIdentifier.kt | 43 ++++++++----------- 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt b/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt index befdb4630..aa096590a 100644 --- a/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt @@ -25,21 +25,15 @@ import org.jetbrains.uast.ULiteralExpression class LiteralTranslationIdentifier : TranslationIdentifier() { override fun identify(element: ULiteralExpression): TranslationInstance? { - val statement = element.uastParent - if (statement != null && element.value is String) { - val project = element.sourcePsi?.project - ?: return null - val result = identify(project, element, statement, element) - return result?.copy( - key = result.key.copy( - infix = result.key.infix.replace( - CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, - "", - ), - ), - ) + val statement = element.uastParent ?: return null + if (element.value !is String) { + return null } - return null + + val project = element.sourcePsi?.project ?: return null + val result = identify(project, element, statement, element) ?: return null + val infix = result.key.infix.replace(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, "") + return result.copy(key = result.key.copy(infix = infix)) } override fun elementClass(): Class = ULiteralExpression::class.java diff --git a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt index 99fc8c3bf..10caae8be 100644 --- a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt @@ -21,43 +21,34 @@ package com.demonwav.mcdev.translations.identification import com.intellij.codeInsight.completion.CompletionUtilCore -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.impl.source.PsiClassReferenceType -import com.intellij.psi.search.GlobalSearchScope -import org.jetbrains.uast.UField +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiType import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.UVariable import org.jetbrains.uast.resolveToUElement class ReferenceTranslationIdentifier : TranslationIdentifier() { override fun identify(element: UReferenceExpression): TranslationInstance? { - val reference = element.resolveToUElement() ?: return null val statement = element.uastParent ?: return null val project = element.sourcePsi?.project ?: return null + val reference = element.resolveToUElement() as? UVariable ?: return null + if (!reference.isFinal) { + return null + } - if (reference is UField) { - val scope = GlobalSearchScope.allScope(project) - val stringClass = - JavaPsiFacade.getInstance(project).findClass("java.lang.String", scope) ?: return null - val isConstant = reference.isStatic && reference.isFinal - val type = reference.type as? PsiClassReferenceType ?: return null - val resolved = type.resolve() ?: return null - if (isConstant && (resolved.isEquivalentTo(stringClass) || resolved.isInheritor(stringClass, true))) { - val referenceElement = reference.uastInitializer as? ULiteralExpression ?: return null - val result = identify(project, element, statement, referenceElement) - - return result?.copy( - key = result.key.copy( - infix = result.key.infix.replace( - CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, - "", - ), - ), - ) - } + val resolveScope = element.sourcePsi?.resolveScope ?: return null + val psiManager = PsiManager.getInstance(project) + val stringType = PsiType.getJavaLangString(psiManager, resolveScope) + if (!stringType.isAssignableFrom(reference.type)) { + return null } - return null + val referenceElement = reference.uastInitializer as? ULiteralExpression ?: return null + val result = identify(project, element, statement, referenceElement) ?: return null + + val infix = result.key.infix.replace(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, "") + return result.copy(key = result.key.copy(infix = infix)) } override fun elementClass(): Class = UReferenceExpression::class.java From bef547ed8cd90dfc824b1237e452f307c05139f9 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 10 Jul 2024 15:23:13 +0200 Subject: [PATCH 18/37] Translation: Make ref identifiers more lenient Now supports polyadic expression evaluation --- .../identification/ReferenceTranslationIdentifier.kt | 3 +-- src/main/kotlin/util/expression-utils.kt | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt index 10caae8be..95ee709fe 100644 --- a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt @@ -23,7 +23,6 @@ package com.demonwav.mcdev.translations.identification import com.intellij.codeInsight.completion.CompletionUtilCore import com.intellij.psi.PsiManager import com.intellij.psi.PsiType -import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.UReferenceExpression import org.jetbrains.uast.UVariable import org.jetbrains.uast.resolveToUElement @@ -44,7 +43,7 @@ class ReferenceTranslationIdentifier : TranslationIdentifier { - val reference = expr.resolve() + val reference = expr.resolveToUElement() if (reference is UVariable && reference.uastInitializer != null) { return eval(reference.uastInitializer, "\${${expr.asSourceString()}}") } } - expr is ULiteralExpression -> - return expr.value.toString() - expr is UCallExpression && allowTranslations -> for (argument in expr.valueArguments) { val translation = TranslationInstance.find(argument) ?: continue @@ -61,6 +59,8 @@ fun UExpression.evaluate(allowReferences: Boolean, allowTranslations: Boolean): return translation.text } + + else -> expr?.evaluateString()?.let { return it } } return if (allowReferences && expr != null) { From 71a946e8d6a30449ef4d409921322c3052187813 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 10 Jul 2024 16:17:34 +0200 Subject: [PATCH 19/37] Translation: Remove superfluous "Convert to translation" action --- .../actions/ConvertToTranslationAction.kt | 51 ------------------- src/main/resources/META-INF/plugin.xml | 5 -- 2 files changed, 56 deletions(-) delete mode 100644 src/main/kotlin/translations/actions/ConvertToTranslationAction.kt diff --git a/src/main/kotlin/translations/actions/ConvertToTranslationAction.kt b/src/main/kotlin/translations/actions/ConvertToTranslationAction.kt deleted file mode 100644 index 9dc71689f..000000000 --- a/src/main/kotlin/translations/actions/ConvertToTranslationAction.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Minecraft Development for IntelliJ - * - * https://mcdev.io/ - * - * Copyright (C) 2024 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.translations.actions - -import com.demonwav.mcdev.translations.intentions.ConvertToTranslationIntention -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.LangDataKeys -import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.psi.PsiLiteral - -class ConvertToTranslationAction : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - val file = e.getData(LangDataKeys.PSI_FILE) ?: return - val editor = e.getData(PlatformDataKeys.EDITOR) ?: return - val element = file.findElementAt(editor.caretModel.offset) ?: return - ConvertToTranslationIntention().invoke(editor.project ?: return, editor, element) - } - - override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT - - override fun update(e: AnActionEvent) { - val file = e.getData(LangDataKeys.PSI_FILE) - val editor = e.getData(PlatformDataKeys.EDITOR) - if (file == null || editor == null) { - e.presentation.isEnabledAndVisible = false - return - } - val element = file.findElementAt(editor.caretModel.offset) - e.presentation.isEnabledAndVisible = (element?.parent as? PsiLiteral)?.value is String - } -} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e8dae1cc0..901b2e43b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1213,11 +1213,6 @@ description="Lookup MCP mapping info on a SRG field or method"> - - - From 9417b4b55c279daab2a0a70cebc4a115946716a1 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 10 Jul 2024 16:18:20 +0200 Subject: [PATCH 20/37] Translation: Ensure we fully fold qualified calls --- .../translations/identification/TranslationIdentifier.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/translations/identification/TranslationIdentifier.kt b/src/main/kotlin/translations/identification/TranslationIdentifier.kt index 766a8886e..68c2babfa 100644 --- a/src/main/kotlin/translations/identification/TranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/TranslationIdentifier.kt @@ -47,6 +47,7 @@ import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.UExpression import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UQualifiedReferenceExpression import org.jetbrains.uast.evaluateString import org.jetbrains.uast.getContainingUClass @@ -119,7 +120,8 @@ abstract class TranslationIdentifier { ?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) == true val foldingElement = if (foldMethod) { - call + // Make sure qualifiers, like I18n in 'I18n.translate()' is also folded + call.uastParent as? UQualifiedReferenceExpression ?: call } else if ( index == 0 && container.valueArgumentCount > 1 && From 52c1f16da708b061c214b18bbda62014f38c038b Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 10 Jul 2024 16:19:02 +0200 Subject: [PATCH 21/37] Translation: Support evaluating qualified i18n calls --- src/main/kotlin/util/expression-utils.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/kotlin/util/expression-utils.kt b/src/main/kotlin/util/expression-utils.kt index caaba2c48..9dca724b2 100644 --- a/src/main/kotlin/util/expression-utils.kt +++ b/src/main/kotlin/util/expression-utils.kt @@ -25,6 +25,7 @@ import com.demonwav.mcdev.translations.identification.TranslationInstance.Compan import org.jetbrains.uast.UBinaryExpressionWithType import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UQualifiedReferenceExpression import org.jetbrains.uast.UReferenceExpression import org.jetbrains.uast.UVariable import org.jetbrains.uast.evaluateString @@ -43,6 +44,13 @@ fun UExpression.evaluate(allowReferences: Boolean, allowTranslations: Boolean): expr is UBinaryExpressionWithType && expr.isTypeCast() -> return eval(expr.operand, defaultValue) + expr is UQualifiedReferenceExpression -> { + val selector = expr.selector + if (selector is UCallExpression) { + return eval(selector, "\${${expr.asSourceString()}}") + } + } + expr is UReferenceExpression -> { val reference = expr.resolveToUElement() if (reference is UVariable && reference.uastInitializer != null) { From b5dc84ca20a2c4009d0365b568245e5a20d9f77c Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 10 Jul 2024 17:23:47 +0200 Subject: [PATCH 22/37] Translation: ConvertToTranslationIntention to UAST --- .../ConvertToTranslationIntention.kt | 151 ++++++++++-------- src/main/resources/META-INF/plugin.xml | 2 +- 2 files changed, 83 insertions(+), 70 deletions(-) diff --git a/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt b/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt index 9db73187d..f03fc5785 100644 --- a/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt +++ b/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt @@ -26,100 +26,113 @@ import com.demonwav.mcdev.translations.TranslationFiles import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.runWriteAction import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction -import com.intellij.lang.java.JavaLanguage import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.ui.InputValidatorEx import com.intellij.openapi.ui.Messages -import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement -import com.intellij.psi.PsiLiteral -import com.intellij.psi.codeStyle.JavaCodeStyleManager import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.findUElementAt +import org.jetbrains.uast.generate.generationPlugin +import org.jetbrains.uast.textRange +import org.jetbrains.uast.toUElementOfType class ConvertToTranslationIntention : PsiElementBaseIntentionAction() { @Throws(IncorrectOperationException::class) override fun invoke(project: Project, editor: Editor, element: PsiElement) { - if (element.parent is PsiLiteral) { - val value = (element.parent as PsiLiteral).value as? String ?: return + val literal = element.parent.toUElementOfType() ?: return + val value = literal.evaluateString() ?: return - val existingKey = TranslationFiles.findTranslationKeyForText(element, value) + val existingKey = TranslationFiles.findTranslationKeyForText(element, value) - val result = Messages.showInputDialogWithCheckBox( - "Enter translation key:", - "Convert String Literal to Translation", - "Replace literal with call to I18n (only works on clients!)", - true, - true, - Messages.getQuestionIcon(), - existingKey, - object : InputValidatorEx { - override fun getErrorText(inputString: String): String? { - if (inputString.isEmpty()) { - return "Key must not be empty" - } - if (inputString.contains('=')) { - return "Key must not contain separator character ('=')" - } - return null + val result = Messages.showInputDialogWithCheckBox( + "Enter translation key:", + "Convert String Literal to Translation", + "Replace literal with call to I18n (only works on clients!)", + true, + true, + Messages.getQuestionIcon(), + existingKey, + object : InputValidatorEx { + override fun getErrorText(inputString: String): String? { + if (inputString.isEmpty()) { + return "Key must not be empty" } - - override fun checkInput(inputString: String): Boolean { - return inputString.isNotEmpty() && !inputString.contains('=') + if (inputString.contains('=')) { + return "Key must not contain separator character ('=')" } + return null + } - override fun canClose(inputString: String): Boolean { - return inputString.isNotEmpty() && !inputString.contains('=') - } - }, - ) - val key = result.first ?: return - val replaceLiteral = result.second - try { - if (existingKey != key) { - TranslationFiles.add(element, key, value) + override fun checkInput(inputString: String): Boolean { + return inputString.isNotEmpty() && !inputString.contains('=') } - if (replaceLiteral) { - val translationSettings = TranslationSettings.getInstance(project) - val psi = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return - psi.runWriteAction { - val expression = JavaPsiFacade.getElementFactory(project).createExpressionFromText( - if (translationSettings.isUseCustomConvertToTranslationTemplate) { - translationSettings.convertToTranslationTemplate.replace("\$key", key) - } else { - element.findModule()?.getMappedMethodCall( - "net.minecraft.client.resource.language.I18n", - "translate", - "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", - "\"$key\"" - ) ?: "net.minecraft.client.resource.I18n.get(\"$key\")" - }, - element.context, - ) - if (psi.language === JavaLanguage.INSTANCE) { - JavaCodeStyleManager.getInstance(project) - .shortenClassReferences(element.parent.replace(expression)) - } else { - element.parent.replace(expression) - } + + override fun canClose(inputString: String): Boolean { + return inputString.isNotEmpty() && !inputString.contains('=') + } + }, + ) + val key = result.first ?: return + val replaceLiteral = result.second + try { + if (existingKey != key) { + TranslationFiles.add(element, key, value) + } + if (replaceLiteral) { + val translationSettings = TranslationSettings.getInstance(project) + val documentManager = PsiDocumentManager.getInstance(project) + val psi = documentManager.getPsiFile(editor.document) ?: return + val callCode = if (translationSettings.isUseCustomConvertToTranslationTemplate) { + translationSettings.convertToTranslationTemplate.replace("\$key", key) + } else { + element.findModule()?.getMappedMethodCall( + "net.minecraft.client.resource.language.I18n", + "translate", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", + "\"$key\"" + ) ?: "net.minecraft.client.resource.I18n.get(\"$key\")" + } + + val replaceRange = when (literal.lang.id) { + // Special case because in Kotlin, the sourcePsi is a template entry, not the literal itself + "kotlin" -> literal.sourcePsi?.parent?.textRange + else -> literal.textRange + } ?: return + + psi.runWriteAction { + // There is no convenient way to generate a qualified call expression with the UAST factory + // so we simply put the raw code there and assume it's correct + editor.document.replaceString(replaceRange.startOffset, replaceRange.endOffset, callCode) + documentManager.commitDocument(editor.document) + + val callOffset = replaceRange.startOffset + callCode.indexOf('(') + val newExpr = psi.findUElementAt(callOffset - 1, UReferenceExpression::class.java) + if (newExpr != null) { + literal.generationPlugin?.shortenReference(newExpr) } } - } catch (e: Exception) { - Notification( - "Translation support error", - "Error while adding translation", - e.message ?: e.stackTraceToString(), - NotificationType.WARNING, - ).notify(project) } + } catch (e: Exception) { + Notification( + "Translation support error", + "Error while adding translation", + e.message ?: e.stackTraceToString(), + NotificationType.WARNING, + ).notify(project) } } - override fun isAvailable(project: Project, editor: Editor, element: PsiElement) = - (element.parent as? PsiLiteral)?.value is String + override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean { + val literal = element.parent.toUElementOfType() + return literal?.evaluateString() is String + } override fun getFamilyName() = "Convert string literal to translation" diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 901b2e43b..e208da41b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -267,7 +267,7 @@ - JAVA + UAST com.demonwav.mcdev.translations.intentions.ConvertToTranslationIntention Minecraft convertToTranslation From 7acd2f0e1851c30ad064a2d5a5ff5fcc1971be19 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 10 Jul 2024 19:18:13 +0200 Subject: [PATCH 23/37] Translation: Fix key completion and show default text --- .../kotlin/translations/reference/completion.kt | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/translations/reference/completion.kt b/src/main/kotlin/translations/reference/completion.kt index 0d17bc146..ada0e77b4 100644 --- a/src/main/kotlin/translations/reference/completion.kt +++ b/src/main/kotlin/translations/reference/completion.kt @@ -38,35 +38,28 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.json.JsonElementTypes import com.intellij.json.JsonLanguage import com.intellij.json.psi.JsonStringLiteral +import com.intellij.openapi.util.text.StringUtil import com.intellij.patterns.PlatformPatterns import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiUtilCore sealed class TranslationCompletionContributor : CompletionContributor() { protected fun handleKey(text: String, element: PsiElement, domain: String?, result: CompletionResultSet) { - if (text.isEmpty()) { - return - } - val defaultEntries = TranslationIndex.getAllDefaultTranslations(element.project, domain) - val existingKeys = TranslationIndex.getTranslations(element.containingFile ?: return).map { it.key }.toSet() + val availableKeys = TranslationIndex.getTranslations(element.containingFile.originalFile).map { it.key }.toSet() val prefixResult = result.withPrefixMatcher(text) - var counter = 0 for (entry in defaultEntries) { val key = entry.key - if (!key.contains(text) || existingKeys.contains(key)) { + if (!key.contains(text) || availableKeys.contains(key)) { continue } - if (counter++ > 1000) { - break // Prevent insane CPU usage - } - + val textHint = StringUtil.shortenTextWithEllipsis(entry.text, 30, 0) prefixResult.addElement( PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(key).withIcon(PlatformAssets.MINECRAFT_ICON), + LookupElementBuilder.create(key).withIcon(PlatformAssets.MINECRAFT_ICON).withTypeText(textHint), 1.0 + key.getSimilarity(text), ), ) From 85e1bcdc797dd222b64d1a6018472ddab344e568 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 11 Jul 2024 13:36:51 +0200 Subject: [PATCH 24/37] Translation: Fix Ktlint and extract missing types hint to field --- .../translations/inspections/MissingFormatInspection.kt | 5 ++++- .../translations/inspections/NoTranslationInspection.kt | 5 ++++- .../inspections/WrongTypeInTranslationArgsInspection.kt | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/translations/inspections/MissingFormatInspection.kt b/src/main/kotlin/translations/inspections/MissingFormatInspection.kt index 65eb0b74f..45f5d6655 100644 --- a/src/main/kotlin/translations/inspections/MissingFormatInspection.kt +++ b/src/main/kotlin/translations/inspections/MissingFormatInspection.kt @@ -26,6 +26,7 @@ import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.intellij.uast.UastHintedVisitorAdapter +import org.jetbrains.uast.UElement import org.jetbrains.uast.UExpression import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor @@ -33,8 +34,10 @@ import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class MissingFormatInspection : TranslationInspection() { override fun getStaticDescription() = "Detects missing format arguments for translations" + private val typesHint: Array> = arrayOf(UExpression::class.java) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = - UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), arrayOf(UExpression::class.java)) + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { diff --git a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt index d1f931636..4ac1c2295 100644 --- a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt +++ b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt @@ -32,6 +32,7 @@ import com.intellij.openapi.ui.Messages import com.intellij.psi.PsiElementVisitor import com.intellij.uast.UastHintedVisitorAdapter import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.UElement import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.toUElementOfType import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor @@ -41,8 +42,10 @@ class NoTranslationInspection : TranslationInspection() { "Checks whether a translation key used in calls to StatCollector.translateToLocal(), " + "StatCollector.translateToLocalFormatted() or I18n.format() exists." + private val typesHint: Array> = arrayOf(ULiteralExpression::class.java) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = - UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), arrayOf(ULiteralExpression::class.java)) + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { diff --git a/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt b/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt index 370ddf2c9..da210a3d6 100644 --- a/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt +++ b/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt @@ -58,7 +58,6 @@ import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class WrongTypeInTranslationArgsInspection : TranslationInspection() { override fun getStaticDescription() = "Detect wrong argument types in translation arguments" - private val typesHint: Array> = arrayOf(UReferenceExpression::class.java, ULiteralExpression::class.java) From 85e493a9ccd22882fa306b17b043e781b727ae1f Mon Sep 17 00:00:00 2001 From: RedNesto Date: Fri, 12 Jul 2024 19:40:07 +0200 Subject: [PATCH 25/37] Repo-based creator templates (#2304) * Initial custom template system * Add remember, editable and property derivation * Add hidden properties * Implement property derivation for all types * Actual types implementation Also fix template condition evaluation * Some more stuff * Some more refactoring to get things working nicely * Move CreatorProperties to an EP * Add property UI order * Move custom template to a separate module builder * Add default values to template descriptor * Add option to output null value if default * Add group/collapsibleGroup support * Dropdown labels * Use segmented buttons for options by default * Support comma separated string lists * Add TemplateProviders * WIP Sponge creator * Support built-in templates * Support multiple templates per provider * Remove commented code * Remember used templates * Move CustomPlatformStep to the appropriate package * Fix recent template provider being saved in the recent list Also always show the templates list in recent templates * Switch BuiltInTemplateProvider to flat dir * Add NeoForge specific stuff * Add TemplateApi marker annotation for template models * Move RecentProjectTemplates out of the models package * Remove old commented code * Replace usage of kotlin plugin function by stdlib one * Always refresh template files * Add fabric_versions * Add license property * Handle template descriptor deserialization errors * Basic template inheritance and template labels * Add basic versioning * Display all yarn/fabric api versions if none match the selected game version * Add property validation support * Don't even call buildUi if property is hidden * Add "select" derivation * Fix templates not getting access to builtin properties * Include license displayname in LicenseData * Add 1.16 & 1.20.6 to MinecraftVersions * Remove unused class * Some ClassFqn doc & withClassName * Add ForgeVersions * Allow to get template from outside the template root * Builtin templates update * Add templates repo as resource modules Helps with template completion using velocity implicit hints * Flatten a bit the builtin template update code * Ktlint fixes * Add licenses * Revert unneeded change * Make properties & files properly optional Also log when a template cannot be loaded because of and unhandled version * Restore required nonnull assert * Run gradle wrapper task after project import * Add .gitignore and git add generated files after gradle wrapper task * Architectury template * Add paper manifest warning * Fix ktlint warnings * Include templates repo as submodule * Include templates in publish workflow * Bump templates submodule * Switch builtin url to org repo * Fix directory name in builtin provider * Explicitly import Gradle and Maven projects * Remove unused imports * Use org repo * Promote new wizard I'd like to keep the old one for some time until all the new templates are proven to be fully working * Actually use the correct org name I swear... * Get rid of AbstractLongRunningAssetsStep usage Also improve robustness of the creator * Reformat and open main files * Remove unused import * Specify TemplateApi target and retention * Improve loading UI/UX * Localization support * Display validation and unhandled errors to user * Split templates into groups * Bump templates * Add user-configurable repositories instead of raw providers * Add back builtin provider * Remove recent templates related code * Convert recursive virtualfile loop into visitor * Make provider label a property and localize it * Move repo table code outside of MinecraftConfigurable * Fix differences in creator properties naming and ctor parameters order * Some work towards more extensible derivations * Add missing licenses * Remove unused imports * Get rid of builtin sponge specific derivation * Rework how all derivations work, with validation now! * Fix imports, again * MavenArtifactVersion should load versions in setupProperty * Invert hidden -> visible and added custom visibility conditions * Add build coords default group and version * Add rawVersionFilter to maven artifact version property * Add versionFilter to maven artifact version property * Fix dropdown default values and add validation to ensure selection is valid * Add Bungeecord and Spigot Kotlin templates Also fix IntegerCreatorProperty default value * Fix Parchment property not matching first release of a major mc version * NeoForge Kotlin templates * Add $version placeholder to remote url * Fixup github archive matcher * Add Fabric split sources * Use Neo's ModDev plugin in 1.21 * Improve template error reporting * Fix Loom's default version selection * No longer unzip remote templates Instead read directly inside them, and allow to configure a different repo root in cases like GitHub's zips, that have a root directory named after the repo and branch name * Cache downloaded versions * Remove superfluous blank line * Actually add the builtin repo by default * Hide the repositories row if only one repo is configured * Proper module generation for finalizers * Update templates submodule * Add customizable storage keys * Rename FabricApi -> Fabric API and ArchitecturyApi -> Architectury API * Remove dead code * Add versions download indicator --- .github/workflows/publish.yml | 4 + .gitmodules | 4 + build.gradle.kts | 27 +- src/main/kotlin/MinecraftConfigurable.kt | 15 + src/main/kotlin/MinecraftSettings.kt | 34 ++ .../kotlin/creator/MinecraftModuleBuilder.kt | 2 +- .../ProjectSetupFinalizerWizardStep.kt | 4 +- .../buildsystem/AbstractBuildSystemStep.kt | 2 +- .../buildsystem/BuildSystemPropertiesStep.kt | 2 +- src/main/kotlin/creator/creator-utils.kt | 16 + .../creator/custom/BuiltinValidations.kt | 78 +++ .../custom/CreatorProgressIndicator.kt | 58 ++ .../custom/CustomMinecraftModuleBuilder.kt | 58 ++ .../creator/custom/CustomPlatformStep.kt | 567 ++++++++++++++++++ .../EvaluateTemplateExpressionAction.kt | 81 +++ .../custom/ResourceBundleTranslator.kt | 47 ++ .../creator/custom/TemplateDescriptor.kt | 99 +++ .../creator/custom/TemplateEvaluator.kt | 52 ++ .../creator/custom/TemplateRepoTable.kt | 133 ++++ .../creator/custom/TemplateResourceBundle.kt | 32 + .../custom/TemplateValidationReporter.kt | 106 ++++ ...ractVersionMajorMinorPropertyDerivation.kt | 66 ++ .../custom/derivation/PropertyDerivation.kt | 38 ++ ...vaVersionForMcVersionPropertyDerivation.kt | 74 +++ .../derivation/ReplacePropertyDerivation.kt | 94 +++ .../derivation/SelectPropertyDerivation.kt | 67 +++ .../SuggestClassNamePropertyDerivation.kt | 68 +++ .../custom/finalizers/CreatorFinalizer.kt | 121 ++++ .../custom/finalizers/GitAddAllFinalizer.kt | 32 + .../ImportGradleProjectFinalizer.kt | 40 ++ .../finalizers/ImportMavenProjectFinalizer.kt | 57 ++ .../finalizers/RunGradleTasksFinalizer.kt | 54 ++ .../custom/model/ArchitecturyVersionsModel.kt | 60 ++ .../custom/model/BuildSystemCoordinates.kt | 27 + .../kotlin/creator/custom/model/ClassFqn.kt | 51 ++ .../kotlin/creator/custom/model/CreatorJdk.kt | 31 + .../custom/model/FabricVersionsModel.kt | 35 ++ .../creator/custom/model/ForgeVersions.kt | 44 ++ .../custom/model/HasMinecraftVersion.kt | 29 + .../creator/custom/model/LicenseData.kt | 30 + .../creator/custom/model/NeoForgeVersions.kt | 46 ++ .../creator/custom/model/ParchmentVersions.kt | 32 + .../kotlin/creator/custom/model/StringList.kt | 31 + .../creator/custom/model/TemplateApi.kt | 30 + .../providers/BuiltinTemplateProvider.kt | 92 +++ .../custom/providers/EmptyLoadedTemplate.kt | 40 ++ .../custom/providers/LoadedTemplate.kt | 33 + .../custom/providers/LocalTemplateProvider.kt | 94 +++ .../providers/RemoteTemplateProvider.kt | 228 +++++++ .../custom/providers/TemplateProvider.kt | 226 +++++++ .../custom/providers/VfsLoadedTemplate.kt | 43 ++ .../custom/providers/ZipTemplateProvider.kt | 92 +++ .../ArchitecturyVersionsCreatorProperty.kt | 481 +++++++++++++++ .../custom/types/BooleanCreatorProperty.kt | 67 +++ .../BuildSystemCoordinatesCreatorProperty.kt | 135 +++++ .../custom/types/ClassFqnCreatorProperty.kt | 78 +++ .../creator/custom/types/CreatorProperty.kt | 288 +++++++++ .../custom/types/CreatorPropertyFactory.kt | 73 +++ .../custom/types/ExternalCreatorProperty.kt | 50 ++ .../types/FabricVersionsCreatorProperty.kt | 350 +++++++++++ .../types/ForgeVersionsCreatorProperty.kt | 219 +++++++ .../types/InlineStringListCreatorProperty.kt | 62 ++ .../custom/types/IntegerCreatorProperty.kt | 82 +++ .../custom/types/JdkCreatorProperty.kt | 77 +++ .../custom/types/LicenseCreatorProperty.kt | 74 +++ .../MavenArtifactVersionCreatorProperty.kt | 177 ++++++ .../types/NeoForgeVersionsCreatorProperty.kt | 211 +++++++ .../custom/types/ParchmentCreatorProperty.kt | 281 +++++++++ .../types/SemanticVersionCreatorProperty.kt | 86 +++ .../custom/types/SimpleCreatorProperty.kt | 134 +++++ .../custom/types/StringCreatorProperty.kt | 103 ++++ src/main/kotlin/creator/step/UseMixinsStep.kt | 2 +- .../architectury/ArchitecturyVersion.kt | 10 +- .../platform/fabric/util/FabricVersions.kt | 2 + .../neoforge/version/NeoModDevVersion.kt | 50 ++ src/main/kotlin/util/MinecraftVersions.kt | 2 + src/main/kotlin/util/files.kt | 7 +- src/main/resources/META-INF/plugin.xml | 40 ++ .../messages/MinecraftDevelopment.properties | 86 ++- .../MinecraftDevelopment_zh.properties | 4 +- templates | 1 + 81 files changed, 6633 insertions(+), 25 deletions(-) create mode 100644 .gitmodules create mode 100644 src/main/kotlin/creator/custom/BuiltinValidations.kt create mode 100644 src/main/kotlin/creator/custom/CreatorProgressIndicator.kt create mode 100644 src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt create mode 100644 src/main/kotlin/creator/custom/CustomPlatformStep.kt create mode 100644 src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt create mode 100644 src/main/kotlin/creator/custom/ResourceBundleTranslator.kt create mode 100644 src/main/kotlin/creator/custom/TemplateDescriptor.kt create mode 100644 src/main/kotlin/creator/custom/TemplateEvaluator.kt create mode 100644 src/main/kotlin/creator/custom/TemplateRepoTable.kt create mode 100644 src/main/kotlin/creator/custom/TemplateResourceBundle.kt create mode 100644 src/main/kotlin/creator/custom/TemplateValidationReporter.kt create mode 100644 src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt create mode 100644 src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt create mode 100644 src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt create mode 100644 src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt create mode 100644 src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt create mode 100644 src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt create mode 100644 src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt create mode 100644 src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt create mode 100644 src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt create mode 100644 src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt create mode 100644 src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt create mode 100644 src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt create mode 100644 src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt create mode 100644 src/main/kotlin/creator/custom/model/ClassFqn.kt create mode 100644 src/main/kotlin/creator/custom/model/CreatorJdk.kt create mode 100644 src/main/kotlin/creator/custom/model/FabricVersionsModel.kt create mode 100644 src/main/kotlin/creator/custom/model/ForgeVersions.kt create mode 100644 src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt create mode 100644 src/main/kotlin/creator/custom/model/LicenseData.kt create mode 100644 src/main/kotlin/creator/custom/model/NeoForgeVersions.kt create mode 100644 src/main/kotlin/creator/custom/model/ParchmentVersions.kt create mode 100644 src/main/kotlin/creator/custom/model/StringList.kt create mode 100644 src/main/kotlin/creator/custom/model/TemplateApi.kt create mode 100644 src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt create mode 100644 src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt create mode 100644 src/main/kotlin/creator/custom/providers/LoadedTemplate.kt create mode 100644 src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt create mode 100644 src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt create mode 100644 src/main/kotlin/creator/custom/providers/TemplateProvider.kt create mode 100644 src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt create mode 100644 src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt create mode 100644 src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/CreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt create mode 100644 src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt create mode 100644 src/main/kotlin/creator/custom/types/StringCreatorProperty.kt create mode 100644 src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt create mode 160000 templates diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f58ca5f03..b0908e67e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,6 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: true + - name: Fetch latest submodule updates + run: git submodule update --remote - uses: actions/setup-java@v3 with: distribution: 'zulu' diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..ed1c5f036 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "templates"] + path = templates + branch = main + url = https://github.com/minecraft-dev/templates diff --git a/build.gradle.kts b/build.gradle.kts index a47d000e1..48e4ab67b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -72,6 +72,26 @@ val gradleToolingExtensionJar = tasks.register(gradleToolingExtensionSource archiveClassifier.set("gradle-tooling-extension") } +val templatesSourceSet: SourceSet = sourceSets.create("templates") { + resources { + srcDir("templates") + compileClasspath += sourceSets.main.get().output + } +} + +val templateSourceSets: List = (file("templates").listFiles() ?: emptyArray()).mapNotNull { file -> + if (file.isDirectory() && (file.listFiles() ?: emptyArray()).any { it.name.endsWith(".mcdev.template.json") }) { + sourceSets.create("templates-${file.name}") { + resources { + srcDir(file) + compileClasspath += sourceSets.main.get().output + } + } + } else { + null + } +} + val externalAnnotationsJar = tasks.register("externalAnnotationsJar") { from("externalAnnotations") destinationDirectory.set(layout.buildDirectory.dir("externalAnnotations")) @@ -381,6 +401,9 @@ tasks.withType { from(externalAnnotationsJar) { into("Minecraft Development/lib/resources") } + from("templates") { + into("Minecraft Development/lib/resources/builtin-templates") + } } tasks.runIde { @@ -391,8 +414,8 @@ tasks.runIde { systemProperty("idea.debug.mode", "true") } // Set these properties to test different languages - // systemProperty("user.language", "en") - // systemProperty("user.country", "US") + systemProperty("user.language", "fr") + systemProperty("user.country", "FR") } tasks.buildSearchableOptions { diff --git a/src/main/kotlin/MinecraftConfigurable.kt b/src/main/kotlin/MinecraftConfigurable.kt index 815131f7f..60f5a32ab 100644 --- a/src/main/kotlin/MinecraftConfigurable.kt +++ b/src/main/kotlin/MinecraftConfigurable.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.creator.custom.templateRepoTable import com.demonwav.mcdev.update.ConfigurePluginUpdatesDialog import com.intellij.ide.projectView.ProjectView import com.intellij.openapi.options.Configurable @@ -31,6 +32,7 @@ import com.intellij.ui.EnumComboBoxModel import com.intellij.ui.components.Label import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.MutableProperty import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel @@ -91,6 +93,19 @@ class MinecraftConfigurable : Configurable { } } + group(MCDevBundle("minecraft.settings.creator")) { + row(MCDevBundle("minecraft.settings.creator.repos")) {} + + row { + templateRepoTable( + MutableProperty( + { settings.creatorTemplateRepos.toMutableList() }, + { settings.creatorTemplateRepos = it } + ) + ) + }.resizableRow() + } + onApply { for (project in ProjectManager.getInstance().openProjects) { ProjectView.getInstance(project).refresh() diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt index b4b596114..0a924aa64 100644 --- a/src/main/kotlin/MinecraftSettings.kt +++ b/src/main/kotlin/MinecraftSettings.kt @@ -20,11 +20,15 @@ package com.demonwav.mcdev +import com.demonwav.mcdev.asset.MCDevBundle import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.editor.markup.EffectType +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.Tag +import com.intellij.util.xmlb.annotations.Text @State(name = "MinecraftSettings", storages = [Storage("minecraft_dev.xml")]) class MinecraftSettings : PersistentStateComponent { @@ -37,8 +41,29 @@ class MinecraftSettings : PersistentStateComponent { var underlineType: UnderlineType = UnderlineType.DOTTED, var isShadowAnnotationsSameLine: Boolean = true, + + var creatorTemplateRepos: List = listOf(TemplateRepo.makeBuiltinRepo()), ) + @Tag("repo") + data class TemplateRepo( + @get:Attribute("name") + var name: String, + @get:Attribute("provider") + var provider: String, + @get:Text + var data: String + ) { + constructor() : this("", "", "") + + companion object { + + fun makeBuiltinRepo(): TemplateRepo { + return TemplateRepo(MCDevBundle("minecraft.settings.creator.repo.builtin_name"), "builtin", "true") + } + } + } + private var state = State() override fun getState(): State { @@ -47,6 +72,9 @@ class MinecraftSettings : PersistentStateComponent { override fun loadState(state: State) { this.state = state + if (state.creatorTemplateRepos.isEmpty()) { + state.creatorTemplateRepos = listOf() + } } // State mappings @@ -86,6 +114,12 @@ class MinecraftSettings : PersistentStateComponent { state.isShadowAnnotationsSameLine = shadowAnnotationsSameLine } + var creatorTemplateRepos: List + get() = state.creatorTemplateRepos.map { it.copy() } + set(creatorTemplateRepos) { + state.creatorTemplateRepos = creatorTemplateRepos.map { it.copy() } + } + enum class UnderlineType(private val regular: String, val effectType: EffectType) { NORMAL("Normal", EffectType.LINE_UNDERSCORE), diff --git a/src/main/kotlin/creator/MinecraftModuleBuilder.kt b/src/main/kotlin/creator/MinecraftModuleBuilder.kt index 7b3f2318c..a847ccf14 100644 --- a/src/main/kotlin/creator/MinecraftModuleBuilder.kt +++ b/src/main/kotlin/creator/MinecraftModuleBuilder.kt @@ -35,7 +35,7 @@ import com.intellij.openapi.roots.ModifiableRootModel class MinecraftModuleBuilder : AbstractNewProjectWizardBuilder() { - override fun getPresentableName() = "Minecraft" + override fun getPresentableName() = "Minecraft (Old Wizard)" override fun getNodeIcon() = PlatformAssets.MINECRAFT_ICON override fun getGroupName() = "Minecraft" override fun getBuilderId() = "MINECRAFT_MODULE" diff --git a/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt b/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt index 6aa8694bb..6c3b70902 100644 --- a/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt +++ b/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt @@ -126,7 +126,9 @@ class JdkProjectSetupFinalizer( private var preferredJdkLabel: Placeholder? = null private var preferredJdkReason = MCDevBundle("creator.validation.jdk_preferred_default_reason") - var preferredJdk: JavaSdkVersion = JavaSdkVersion.JDK_17 + val preferredJdkProperty = propertyGraph.property(JavaSdkVersion.JDK_17) + + var preferredJdk: JavaSdkVersion by preferredJdkProperty private set fun setPreferredJdk(value: JavaSdkVersion, reason: String) { diff --git a/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt b/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt index 51614b1fb..887682753 100644 --- a/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt +++ b/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt @@ -49,7 +49,7 @@ abstract class AbstractBuildSystemStep( override val self get() = this override val label - get() = MCDevBundle("creator.ui.build_system.label.generic") + get() = MCDevBundle("creator.ui.build_system.label") override fun initSteps(): LinkedHashMap { context.putUserData(PLATFORM_NAME_KEY, platformName) diff --git a/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt b/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt index bc6324f54..67cc8a3ef 100644 --- a/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt +++ b/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt @@ -52,7 +52,7 @@ class BuildSystemPropertiesStep(private val parent: ParentStep) : Ab val groupIdProperty = propertyGraph.property("org.example") .bindStorage("${javaClass.name}.groupId") val artifactIdProperty = propertyGraph.lazyProperty(::suggestArtifactId) - private val versionProperty = propertyGraph.property("1.0-SNAPSHOT") + val versionProperty = propertyGraph.property("1.0-SNAPSHOT") .bindStorage("${javaClass.name}.version") var groupId by groupIdProperty diff --git a/src/main/kotlin/creator/creator-utils.kt b/src/main/kotlin/creator/creator-utils.kt index 687793ddf..a1ab81512 100644 --- a/src/main/kotlin/creator/creator-utils.kt +++ b/src/main/kotlin/creator/creator-utils.kt @@ -26,11 +26,15 @@ import com.demonwav.mcdev.creator.step.LicenseStep import com.demonwav.mcdev.util.MinecraftTemplates import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.starters.local.GeneratorTemplateFile +import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.ide.wizard.AbstractNewProjectWizardStep +import com.intellij.ide.wizard.AbstractWizard import com.intellij.ide.wizard.GitNewProjectWizardData import com.intellij.ide.wizard.NewProjectWizardStep import com.intellij.notification.Notification import com.intellij.notification.NotificationType +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.project.Project @@ -160,3 +164,15 @@ fun notifyCreatedProjectNotOpened() { NotificationType.ERROR, ).notify(null) } + +val WizardContext.modalityState: ModalityState + get() { + val contentPanel = this.getUserData(AbstractWizard.KEY)?.contentPanel + + if (contentPanel == null) { + thisLogger().error("Wizard content panel is null, using default modality state") + return ModalityState.defaultModalityState() + } + + return ModalityState.stateForComponent(contentPanel) + } diff --git a/src/main/kotlin/creator/custom/BuiltinValidations.kt b/src/main/kotlin/creator/custom/BuiltinValidations.kt new file mode 100644 index 000000000..8fd1a8401 --- /dev/null +++ b/src/main/kotlin/creator/custom/BuiltinValidations.kt @@ -0,0 +1,78 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.validation.DialogValidation +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.util.text.StringUtil +import javax.swing.JComponent + +object BuiltinValidations { + val nonBlank = validationErrorIf(MCDevBundle("creator.validation.blank")) { it.isBlank() } + + val validVersion = validationErrorIf(MCDevBundle("creator.validation.semantic_version")) { + SemanticVersion.tryParse(it) == null + } + + val nonEmptyVersion = DialogValidation.WithParameter> { combobox -> + DialogValidation { + if (combobox.item?.parts.isNullOrEmpty()) { + ValidationInfo(MCDevBundle("creator.validation.semantic_version")) + } else { + null + } + } + } + + val nonEmptyYarnVersion = DialogValidation.WithParameter> { combobox -> + DialogValidation { + if (combobox.item == null) { + ValidationInfo(MCDevBundle("creator.validation.semantic_version")) + } else { + null + } + } + } + + val validClassFqn = validationErrorIf(MCDevBundle("creator.validation.class_fqn")) { + it.isBlank() || it.split('.').any { part -> !StringUtil.isJavaIdentifier(part) } + } + + fun byRegex(regex: Regex): DialogValidation.WithParameter<() -> String> = + validationErrorIf(MCDevBundle("creator.validation.regex", regex)) { !it.matches(regex) } + + fun isAnyOf( + selectionGetter: () -> T, + options: Collection, + component: JComponent? = null + ): DialogValidation = DialogValidation { + if (selectionGetter() !in options) { + return@DialogValidation ValidationInfo(MCDevBundle("creator.validation.invalid_option"), component) + } + + return@DialogValidation null + } +} diff --git a/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt b/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt new file mode 100644 index 000000000..2bad9bf12 --- /dev/null +++ b/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt @@ -0,0 +1,58 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.progress.TaskInfo +import com.intellij.openapi.progress.util.ProgressIndicatorBase + +class CreatorProgressIndicator( + val loadingProperty: GraphProperty? = null, + val textProperty: GraphProperty? = null, + val text2Property: GraphProperty? = null, +) : ProgressIndicatorBase(false, false) { + + init { + loadingProperty?.set(false) + textProperty?.set("") + text2Property?.set("") + } + + override fun start() { + super.start() + loadingProperty?.set(true) + } + + override fun finish(task: TaskInfo) { + super.finish(task) + loadingProperty?.set(false) + } + + override fun setText(text: String?) { + super.setText(text) + textProperty?.set(text ?: "") + } + + override fun setText2(text: String?) { + super.setText2(text) + text2Property?.set(text ?: "") + } +} diff --git a/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt b/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt new file mode 100644 index 000000000..65a95052e --- /dev/null +++ b/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt @@ -0,0 +1,58 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.creator.step.NewProjectWizardChainStep.Companion.nextStep +import com.intellij.ide.projectWizard.ProjectSettingsStep +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.ide.wizard.AbstractNewProjectWizardBuilder +import com.intellij.ide.wizard.GitNewProjectWizardStep +import com.intellij.ide.wizard.NewProjectWizardBaseStep +import com.intellij.ide.wizard.RootNewProjectWizardStep +import com.intellij.openapi.roots.ModifiableRootModel + +class CustomMinecraftModuleBuilder : AbstractNewProjectWizardBuilder() { + + override fun getPresentableName() = "Minecraft" + override fun getNodeIcon() = PlatformAssets.MINECRAFT_ICON + override fun getGroupName() = "Minecraft" + override fun getBuilderId() = "CUSTOM_MINECRAFT_MODULE" + override fun getDescription() = MCDevBundle("creator.ui.create_minecraft_project") + + override fun setupRootModel(modifiableRootModel: ModifiableRootModel) { + if (moduleJdk != null) { + modifiableRootModel.sdk = moduleJdk + } else { + modifiableRootModel.inheritSdk() + } + } + + override fun getParentGroup() = "Minecraft" + + override fun createStep(context: WizardContext) = RootNewProjectWizardStep(context) + .nextStep(::NewProjectWizardBaseStep) + .nextStep(::GitNewProjectWizardStep) + .nextStep(::CustomPlatformStep) + + override fun getIgnoredSteps() = listOf(ProjectSettingsStep::class.java) +} diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt new file mode 100644 index 000000000..8c54b0bf9 --- /dev/null +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -0,0 +1,567 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.finalizers.CreatorFinalizer +import com.demonwav.mcdev.creator.custom.providers.EmptyLoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.TemplateProvider +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.creator.custom.types.CreatorPropertyFactory +import com.demonwav.mcdev.creator.custom.types.ExternalCreatorProperty +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.toTypedArray +import com.demonwav.mcdev.util.virtualFileOrError +import com.intellij.codeInsight.actions.ReformatCodeProcessor +import com.intellij.ide.projectView.ProjectView +import com.intellij.ide.wizard.AbstractNewProjectWizardStep +import com.intellij.ide.wizard.GitNewProjectWizardData +import com.intellij.ide.wizard.NewProjectWizardBaseData +import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.refreshAndFindVirtualFile +import com.intellij.psi.PsiManager +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Placeholder +import com.intellij.ui.dsl.builder.SegmentedButton +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import java.nio.file.Path +import java.util.function.Consumer +import javax.swing.JLabel +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +/** + * The step to select a custom template repo. + */ +class CustomPlatformStep( + parent: NewProjectWizardStep, +) : AbstractNewProjectWizardStep(parent) { + + val templateRepos = MinecraftSettings.instance.creatorTemplateRepos + + val templateRepoProperty = propertyGraph.property( + templateRepos.firstOrNull() ?: MinecraftSettings.TemplateRepo.makeBuiltinRepo() + ) + var templateRepo by templateRepoProperty + + val availableGroupsProperty = propertyGraph.property>(emptyList()) + var availableGroups by availableGroupsProperty + val availableTemplatesProperty = propertyGraph.property>(emptyList()) + var availableTemplates by availableTemplatesProperty + lateinit var availableGroupsSegmentedButton: SegmentedButton + lateinit var availableTemplatesSegmentedButton: SegmentedButton + + val selectedGroupProperty = propertyGraph.property("") + var selectedGroup by selectedGroupProperty + val selectedTemplateProperty = propertyGraph.property(EmptyLoadedTemplate) + var selectedTemplate by selectedTemplateProperty + + val templateProvidersLoadingProperty = propertyGraph.property(true) + val templateProvidersTextProperty = propertyGraph.property("") + val templateProvidersText2Property = propertyGraph.property("") + lateinit var templateProvidersProcessIcon: Cell + + val templateLoadingProperty = propertyGraph.property(true) + val templateLoadingTextProperty = propertyGraph.property("") + val templateLoadingText2Property = propertyGraph.property("") + lateinit var templatePropertiesProcessIcon: Cell + lateinit var noTemplatesAvailable: Cell + var templateLoadingIndicator: ProgressIndicator? = null + + private var hasTemplateErrors: Boolean = true + + private var properties = mutableMapOf>() + + override fun setupUI(builder: Panel) { + lateinit var templatePropertyPlaceholder: Placeholder + + builder.row(MCDevBundle("creator.ui.custom.repos.label")) { + segmentedButton(templateRepos) { it.name } + .bind(templateRepoProperty) + }.visible(templateRepos.size > 1) + + builder.row { + templateProvidersProcessIcon = + cell(AsyncProcessIcon("TemplateProviders init")) + .visibleIf(templateProvidersLoadingProperty) + label(MCDevBundle("creator.step.generic.init_template_providers.message")) + .visibleIf(templateProvidersLoadingProperty) + label("") + .bindText(templateProvidersTextProperty) + .visibleIf(templateProvidersLoadingProperty) + label("") + .bindText(templateProvidersText2Property) + .visibleIf(templateProvidersLoadingProperty) + } + + templateRepoProperty.afterChange { templateRepo -> + templatePropertyPlaceholder.component = null + availableTemplates = emptyList() + loadTemplatesInBackground { + val provider = TemplateProvider.get(templateRepo.provider) + provider?.loadTemplates(context, templateRepo).orEmpty() + } + } + + builder.row(MCDevBundle("creator.ui.custom.groups.label")) { + availableGroupsSegmentedButton = + segmentedButton(emptyList(), String::toString) + .bind(selectedGroupProperty) + }.visibleIf( + availableGroupsProperty.transform { it.size > 1 } + ) + + builder.row(MCDevBundle("creator.ui.custom.templates.label")) { + availableTemplatesSegmentedButton = + segmentedButton(emptyList(), LoadedTemplate::label, LoadedTemplate::tooltip) + .bind(selectedTemplateProperty) + .validation { + addApplyRule("", condition = ::hasTemplateErrors) + } + }.visibleIf( + availableTemplatesProperty.transform { it.size > 1 } + ) + + availableTemplatesProperty.afterChange { newTemplates -> + val groups = newTemplates.mapTo(linkedSetOf()) { it.descriptor.translatedGroup } + availableGroupsSegmentedButton.items(groups) + // availableGroupsSegmentedButton.visible(groups.size > 1) + availableGroups = groups + selectedGroup = groups.firstOrNull() ?: "empty" + } + + selectedGroupProperty.afterChange { group -> + val templates = availableTemplates.filter { it.descriptor.translatedGroup == group } + availableTemplatesSegmentedButton.items(templates) + // Force visiblity because the component might become hidden and not show up again + // when the segmented button switches between dropdown and buttons + availableTemplatesSegmentedButton.visible(true) + templatePropertyPlaceholder.component = null + selectedTemplate = templates.firstOrNull() ?: EmptyLoadedTemplate + } + + selectedTemplateProperty.afterChange { template -> + createOptionsPanelInBackground(template, templatePropertyPlaceholder) + } + + builder.row { + templatePropertiesProcessIcon = + cell(AsyncProcessIcon("Templates loading")) + .visibleIf(templateLoadingProperty) + label(MCDevBundle("creator.step.generic.load_template.message")) + .visibleIf(templateLoadingProperty) + label("") + .bindText(templateLoadingTextProperty) + .visibleIf(templateLoadingProperty) + label("") + .bindText(templateLoadingText2Property) + .visibleIf(templateLoadingProperty) + noTemplatesAvailable = label(MCDevBundle("creator.step.generic.no_templates_available.message")) + .visible(false) + .apply { component.foreground = JBColor.RED } + templatePropertyPlaceholder = placeholder().align(AlignX.FILL) + }.topGap(TopGap.SMALL) + + initTemplates() + } + + private fun initTemplates() { + selectedTemplate = EmptyLoadedTemplate + + val task = object : Task.Backgroundable( + context.project, + MCDevBundle("creator.step.generic.init_template_providers.message"), + true, + ALWAYS_BACKGROUND, + ) { + + override fun run(indicator: ProgressIndicator) { + if (project?.isDisposed == true) { + return + } + + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateProvidersLoadingProperty.set(true) + VirtualFileManager.getInstance().syncRefresh() + }, context.modalityState) + + for ((providerKey, repos) in templateRepos.groupBy { it.provider }) { + ProgressManager.checkCanceled() + val provider = TemplateProvider.get(providerKey) + ?: continue + indicator.text = provider.label + runCatching { provider.init(indicator, repos) } + .getOrLogException(logger()) + } + + ProgressManager.checkCanceled() + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateProvidersLoadingProperty.set(false) + // Force refresh to trigger template loading + templateRepoProperty.set(templateRepo) + }, context.modalityState) + } + } + + val indicator = CreatorProgressIndicator( + templateProvidersLoadingProperty, + templateProvidersTextProperty, + templateProvidersText2Property + ) + ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) + } + + private fun loadTemplatesInBackground(provider: () -> Collection) { + selectedTemplate = EmptyLoadedTemplate + + val task = object : Task.Backgroundable( + context.project, + MCDevBundle("creator.step.generic.load_template.message"), + true, + ALWAYS_BACKGROUND, + ) { + + override fun run(indicator: ProgressIndicator) { + if (project?.isDisposed == true) { + return + } + + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateLoadingProperty.set(true) + VirtualFileManager.getInstance().syncRefresh() + }, context.modalityState) + + ProgressManager.checkCanceled() + val newTemplates = runCatching { provider() } + .getOrLogException(logger()) + ?: emptyList() + + ProgressManager.checkCanceled() + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateLoadingProperty.set(false) + noTemplatesAvailable.visible(newTemplates.isEmpty()) + availableTemplates = newTemplates + }, context.modalityState) + } + } + + templateLoadingIndicator?.cancel() + + val indicator = CreatorProgressIndicator( + templateLoadingProperty, + templateLoadingTextProperty, + templateLoadingText2Property + ) + templateLoadingIndicator = indicator + ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) + } + + private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { + properties = mutableMapOf() + + if (!template.isValid) { + return + } + + val baseData = data.getUserData(NewProjectWizardBaseData.KEY) + ?: return thisLogger().error("Could not find wizard base data") + + properties["PROJECT_NAME"] = ExternalCreatorProperty( + graph = propertyGraph, + properties = properties, + graphProperty = baseData.nameProperty, + valueType = String::class.java + ) + + placeholder.component = panel { + val reporter = TemplateValidationReporterImpl() + val uiFactories = setupTemplate(template, reporter) + if (uiFactories.isEmpty() && !reporter.hasErrors) { + row { + label(MCDevBundle("creator.ui.warn.no_properties")) + .component.foreground = JBColor.YELLOW + } + } else { + hasTemplateErrors = reporter.hasErrors + reporter.display(this) + + if (!reporter.hasErrors) { + for (uiFactory in uiFactories) { + uiFactory.accept(this) + } + } + } + } + } + + private fun setupTemplate( + template: LoadedTemplate, + reporter: TemplateValidationReporterImpl + ): List> { + return try { + val properties = template.descriptor.properties.orEmpty() + .mapNotNull { + reporter.subject = it.name + setupProperty(it, reporter) + } + .sortedBy { (_, order) -> order } + .map { it.first } + + val finalizers = template.descriptor.finalizers + if (finalizers != null) { + CreatorFinalizer.validateAll(reporter, finalizers) + } + + properties + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error( + "Unexpected error during template setup", + t, + template.label, + template.descriptor.toString() + ) + + emptyList() + } finally { + reporter.subject = null + } + } + + private fun setupProperty( + descriptor: TemplatePropertyDescriptor, + reporter: TemplateValidationReporter + ): Pair, Int>? { + if (!descriptor.groupProperties.isNullOrEmpty()) { + val childrenUiFactories = descriptor.groupProperties + .mapNotNull { setupProperty(it, reporter) } + .sortedBy { (_, order) -> order } + .map { it.first } + + val factory = Consumer { panel -> + val label = descriptor.translatedLabel + if (descriptor.collapsible == false) { + panel.group(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@group) + } + } + } else { + val group = panel.collapsibleGroup(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@collapsibleGroup) + } + } + + group.expanded = descriptor.default as? Boolean ?: false + } + } + + val order = descriptor.order ?: 0 + return factory to order + } + + if (descriptor.name in properties.keys) { + reporter.fatal("Duplicate property name ${descriptor.name}") + } + + val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, propertyGraph, properties) + if (prop == null) { + reporter.fatal("Unknown template property type ${descriptor.type}") + } + + prop.setupProperty(reporter) + + properties[descriptor.name] = prop + + if (descriptor.visible == false) { + return null + } + + val factory = Consumer { panel -> prop.buildUi(panel, context) } + val order = descriptor.order ?: 0 + return factory to order + } + + override fun setupProject(project: Project) { + val template = selectedTemplate + if (template is EmptyLoadedTemplate) { + return + } + + val projectPath = context.projectDirectory + val templateProperties = collectTemplateProperties() + thisLogger().debug("Template properties: $templateProperties") + + val generatedFiles = mutableListOf>() + for (file in template.descriptor.files.orEmpty()) { + if (file.condition != null && + !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } + ) { + continue + } + + val relativeTemplate = TemplateEvaluator.template(templateProperties, file.template).getOrNull() + ?: continue + val relativeDest = TemplateEvaluator.template(templateProperties, file.destination).getOrNull() + ?: continue + + try { + val templateContents = template.loadTemplateContents(relativeTemplate) + ?: continue + + val destPath = projectPath.resolve(relativeDest).toAbsolutePath() + if (!destPath.startsWith(projectPath)) { + // We want to make sure template files aren't 'escaping' the project directory + continue + } + + var fileTemplateProperties = templateProperties + if (file.properties != null) { + fileTemplateProperties = templateProperties.toMutableMap() + fileTemplateProperties.putAll(file.properties) + } + + val processedContent = TemplateEvaluator.template(fileTemplateProperties, templateContents) + .onFailure { t -> + val attachment = Attachment(relativeTemplate, templateContents) + thisLogger().error("Failed evaluate template '$relativeTemplate'", t, attachment) + } + .getOrNull() + ?: continue + + destPath.parent.createDirectories() + destPath.writeText(processedContent) + + val virtualFile = destPath.refreshAndFindVirtualFile() + if (virtualFile != null) { + generatedFiles.add(file to virtualFile) + } else { + thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") + } + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to process template file $file", t) + } + } + + application.executeOnPooledThread { + application.invokeLater({ + application.runWriteAction { + LocalFileSystem.getInstance().refresh(false) + // Apparently a module root is required for the reformat to work + setupTempRootModule(project, projectPath) + } + reformatFiles(project, generatedFiles) + openFilesInEditor(project, generatedFiles) + }, project.disposed) + + val finalizers = selectedTemplate.descriptor.finalizers + if (!finalizers.isNullOrEmpty()) { + CreatorFinalizer.executeAll(context, finalizers, templateProperties) + } + } + } + + private fun setupTempRootModule(project: Project, projectPath: Path) { + val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() + val module = modifiableModel.newNonPersistentModule("mcdev-temp-root", ModuleTypeId.JAVA_MODULE) + val rootsModel = ModuleRootManager.getInstance(module).modifiableModel + rootsModel.addContentEntry(projectPath.virtualFileOrError) + rootsModel.commit() + modifiableModel.commit() + } + + private fun collectTemplateProperties(): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + val gitData = data.getUserData(GitNewProjectWizardData.KEY) + into["USE_GIT"] = gitData?.git == true + + return properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } + + private fun reformatFiles( + project: Project, + files: MutableList> + ) { + val psiManager = PsiManager.getInstance(project) + val psiFiles = files.asSequence() + .filter { (desc, _) -> desc.reformat != false } + .mapNotNull { (_, file) -> psiManager.findFile(file) } + ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false).run() + } + + private fun openFilesInEditor( + project: Project, + files: MutableList> + ) { + val fileEditorManager = FileEditorManager.getInstance(project) + val projectView = ProjectView.getInstance(project) + for ((desc, file) in files) { + if (desc.openInEditor == true) { + fileEditorManager.openFile(file, true) + projectView.select(null, file, false) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt b/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt new file mode 100644 index 000000000..d2fc2c771 --- /dev/null +++ b/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt @@ -0,0 +1,81 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class EvaluateTemplateExpressionAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + + val dialog = EvaluateDialog() + dialog.isModal = false + dialog.show() + } + + private class EvaluateDialog : DialogWrapper(null, false, IdeModalityType.IDE) { + val document = EditorFactory.getInstance().createDocument("") + val editor = EditorFactory.getInstance().createEditor(document) as EditorEx + + lateinit var field: JBTextField + + init { + title = "Evaluate Template Expression" + isOKActionEnabled = true + setValidationDelay(0) + + Disposer.register(disposable) { + EditorFactory.getInstance().releaseEditor(editor) + } + + init() + } + + override fun createCenterPanel(): JComponent = panel { + row { + cell(editor.component).align(Align.FILL) + } + + row("Result:") { + field = textField().align(Align.FILL).component + field.isEditable = false + } + } + + override fun doOKAction() { + val props = mapOf( + "BUILD_SYSTEM" to "gradle", + "USE_PAPER_MANIFEST" to false, + "MAIN_CLASS" to ClassFqn("io.github.rednesto.test.Test") + ) + field.text = TemplateEvaluator.evaluate(props, document.text).toString() + } + } +} diff --git a/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt b/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt new file mode 100644 index 000000000..c90077f45 --- /dev/null +++ b/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt @@ -0,0 +1,47 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.intellij.openapi.util.text.StringUtil +import java.util.MissingResourceException +import java.util.ResourceBundle +import org.jetbrains.annotations.Nls +import org.jetbrains.annotations.NonNls + +abstract class ResourceBundleTranslator { + + abstract val bundle: ResourceBundle? + + fun translate(key: @NonNls String): @Nls String { + return translateOrNull(key) ?: StringUtil.escapeMnemonics(key) + } + + fun translateOrNull(key: @NonNls String): @Nls String? { + if (bundle != null) { + try { + return bundle!!.getString(key) + } catch (_: MissingResourceException) { + } + } + return MCDevBundle.messageOrNull(key) + } +} diff --git a/src/main/kotlin/creator/custom/TemplateDescriptor.kt b/src/main/kotlin/creator/custom/TemplateDescriptor.kt new file mode 100644 index 000000000..abed981b6 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateDescriptor.kt @@ -0,0 +1,99 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import java.util.ResourceBundle + +data class TemplateDescriptor( + val version: Int, + val label: String? = null, + val group: String? = null, + val inherit: String? = null, + val hidden: Boolean? = null, + val properties: List? = null, + val files: List? = null, + val finalizers: List>? = null, +) : ResourceBundleTranslator() { + @Transient + override var bundle: ResourceBundle? = null + + val translatedGroup: String + get() = translate("creator.ui.group.${(group ?: "default").lowercase()}.label") + + companion object { + + const val FORMAT_VERSION = 1 + } +} + +data class TemplatePropertyDescriptor( + val name: String, + val type: String, + val label: String? = null, + val order: Int? = null, + val options: Any? = null, + val limit: Int? = null, + val maxSegmentedButtonsCount: Int? = null, + val forceDropdown: Boolean? = null, + val groupProperties: List? = null, + val remember: Any? = null, + val visible: Any? = null, + val editable: Boolean? = null, + val collapsible: Boolean? = null, + val warning: String? = null, + val default: Any?, + val nullIfDefault: Boolean? = null, + val derives: PropertyDerivation? = null, + val inheritFrom: String? = null, + val parameters: Map? = null, + val validator: Any? = null +) : ResourceBundleTranslator() { + @Transient + override var bundle: ResourceBundle? = null + + val translatedLabel: String + get() = translate(label ?: "creator.ui.${name.lowercase()}.label") + val translatedWarning: String? + get() = translateOrNull(label ?: "creator.ui.${name.lowercase()}.warning") ?: warning +} + +data class PropertyDerivation( + val parents: List? = null, + val method: String? = null, + val select: List? = null, + val default: Any? = null, + val whenModified: Boolean? = null, + val parameters: Map? = null, +) + +data class PropertyDerivationSelect( + val condition: String, + val value: Any +) + +data class TemplateFile( + val template: String, + val destination: String, + val condition: String? = null, + val properties: Map? = null, + val reformat: Boolean? = null, + val openInEditor: Boolean? = null, +) diff --git a/src/main/kotlin/creator/custom/TemplateEvaluator.kt b/src/main/kotlin/creator/custom/TemplateEvaluator.kt new file mode 100644 index 000000000..b717fe712 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateEvaluator.kt @@ -0,0 +1,52 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion +import org.apache.velocity.VelocityContext +import org.apache.velocity.app.Velocity +import org.apache.velocity.util.StringBuilderWriter + +object TemplateEvaluator { + + val baseProperties = mapOf( + "semver" to SemanticVersion.Companion, + "mcver" to MinecraftVersions + ) + + fun evaluate(properties: Map, template: String): Result> { + val context = VelocityContext(baseProperties + properties) + val stringWriter = StringBuilderWriter() + return runCatching { + Velocity.evaluate(context, stringWriter, "McDevTplExpr", template) to stringWriter.toString() + } + } + + fun template(properties: Map, template: String): Result { + return evaluate(properties, template).map { it.second } + } + + fun condition(properties: Map, condition: String): Result { + val actualCondition = "#if ($condition) true #else false #end" + return evaluate(properties, actualCondition).map { it.second.trim().toBoolean() } + } +} diff --git a/src/main/kotlin/creator/custom/TemplateRepoTable.kt b/src/main/kotlin/creator/custom/TemplateRepoTable.kt new file mode 100644 index 000000000..d14f023f0 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateRepoTable.kt @@ -0,0 +1,133 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.providers.TemplateProvider +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.ComboBoxTableCellRenderer +import com.intellij.ui.ToolbarDecorator +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.MutableProperty +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.table.TableView +import com.intellij.util.ListWithSelection +import com.intellij.util.ui.ColumnInfo +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.ListTableModel +import com.intellij.util.ui.table.ComboBoxTableCellEditor +import java.awt.Dimension +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.table.TableCellEditor +import javax.swing.table.TableCellRenderer + +private object NameColumn : ColumnInfo( + MCDevBundle("minecraft.settings.creator.repos.column.name") +) { + override fun valueOf(item: MinecraftSettings.TemplateRepo?): String? { + return item?.name + } + + override fun setValue(item: MinecraftSettings.TemplateRepo?, value: String?) { + item?.name = value ?: MCDevBundle("minecraft.settings.creator.repo.default_name") + } + + override fun isCellEditable(item: MinecraftSettings.TemplateRepo?): Boolean = true +} + +private object ProviderColumn : ColumnInfo( + MCDevBundle("minecraft.settings.creator.repos.column.provider") +) { + override fun valueOf(item: MinecraftSettings.TemplateRepo?): ListWithSelection? { + val providers = TemplateProvider.getAllKeys() + val list = ListWithSelection(providers) + list.select(item?.provider?.takeIf(providers::contains)) + + return list + } + + override fun setValue(item: MinecraftSettings.TemplateRepo?, value: Any?) { + item?.provider = value as? String ?: "local" + } + + override fun isCellEditable(item: MinecraftSettings.TemplateRepo?): Boolean = true + + override fun getRenderer(item: MinecraftSettings.TemplateRepo?): TableCellRenderer? { + return ComboBoxTableCellRenderer.INSTANCE + } + + override fun getEditor(item: MinecraftSettings.TemplateRepo?): TableCellEditor? { + return ComboBoxTableCellEditor.INSTANCE + } +} + +fun Row.templateRepoTable( + prop: MutableProperty> +): Cell { + val model = object : ListTableModel(NameColumn, ProviderColumn) { + override fun addRow() { + val defaultName = MCDevBundle("minecraft.settings.creator.repo.default_name") + addRow(MinecraftSettings.TemplateRepo(defaultName, "local", "")) + } + } + + val table = TableView(model) + table.setShowGrid(true) + table.tableHeader.reorderingAllowed = false + + val decoratedTable = ToolbarDecorator.createDecorator(table) + .setPreferredSize(Dimension(JBUI.scale(300), JBUI.scale(200))) + .setEditActionUpdater { + val selectedRepo = table.selection.firstOrNull() + ?: return@setEditActionUpdater false + val provider = TemplateProvider.get(selectedRepo.provider) + ?: return@setEditActionUpdater false + return@setEditActionUpdater provider.hasConfig + } + .setEditAction { + val selectedRepo = table.selection.firstOrNull() + ?: return@setEditAction + val provider = TemplateProvider.get(selectedRepo.provider) + ?: return@setEditAction + val dataConsumer = { data: String -> selectedRepo.data = data } + val configPanel = provider.setupConfigUi(selectedRepo.data, dataConsumer) + ?: return@setEditAction + + val dialog = object : DialogWrapper(table, true) { + init { + init() + } + + override fun createCenterPanel(): JComponent = configPanel + } + dialog.title = MCDevBundle("minecraft.settings.creator.repo_config.title", selectedRepo.name) + dialog.show() + } + .createPanel() + return cell(decoratedTable) + .bind( + { _ -> model.items }, + { _, repos -> model.items = repos; }, + prop + ) +} diff --git a/src/main/kotlin/creator/custom/TemplateResourceBundle.kt b/src/main/kotlin/creator/custom/TemplateResourceBundle.kt new file mode 100644 index 000000000..ecb4d4bf0 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateResourceBundle.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import java.io.Reader +import java.util.PropertyResourceBundle +import java.util.ResourceBundle + +class TemplateResourceBundle(val reader: Reader, parent: ResourceBundle?) : PropertyResourceBundle(reader) { + + init { + this.parent = parent + } +} diff --git a/src/main/kotlin/creator/custom/TemplateValidationReporter.kt b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt new file mode 100644 index 000000000..b953eb16e --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt @@ -0,0 +1,106 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel + +interface TemplateValidationReporter { + + fun warn(message: String) + + fun error(message: String) + + fun fatal(message: String, cause: Throwable? = null): Nothing +} + +class TemplateValidationReporterImpl : TemplateValidationReporter { + + private val validationItems: MutableMap> = linkedMapOf() + var hasErrors = false + private set + var hasWarns = false + private set + + var subject: String? = null + + override fun warn(message: String) { + check(subject != null) { "No subject is being validated" } + hasWarns = true + validationItems.getOrPut(subject!!, ::mutableListOf).add(TemplateValidationItem.Warn(message)) + } + + override fun error(message: String) { + check(subject != null) { "No subject is being validated" } + hasErrors = true + validationItems.getOrPut(subject!!, ::mutableListOf).add(TemplateValidationItem.Error(message)) + } + + override fun fatal(message: String, cause: Throwable?): Nothing { + error("Fatal validation error: $message") + throw TemplateValidationException(message, cause) + } + + fun display(panel: Panel) { + if (!hasErrors && !hasWarns) { + return + } + + panel.row { + when { + hasWarns && hasErrors -> label(MCDevBundle("creator.ui.error.template_warns_and_errors")).apply { + component.foreground = JBColor.RED + } + + hasWarns -> label(MCDevBundle("creator.ui.error.template_warns")).apply { + component.foreground = JBColor.YELLOW + } + + hasErrors -> label(MCDevBundle("creator.ui.error.template_errors")).apply { + component.foreground = JBColor.RED + } + } + } + + for ((subjectName, items) in validationItems) { + panel.row { + label("$subjectName:") + } + + panel.indent { + for (item in items) { + row { + label(item.message).component.foreground = item.color + } + } + } + } + } +} + +class TemplateValidationException(message: String?, cause: Throwable? = null) : Exception(message, cause) + +private sealed class TemplateValidationItem(val message: String, val color: JBColor) { + + class Warn(message: String) : TemplateValidationItem(message, JBColor.YELLOW) + class Error(message: String) : TemplateValidationItem(message, JBColor.RED) +} diff --git a/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt new file mode 100644 index 000000000..186117050 --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt @@ -0,0 +1,66 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.SemanticVersion + +class ExtractVersionMajorMinorPropertyDerivation : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val from = parentValues[0] as SemanticVersion + if (from.parts.size < 2) { + return SemanticVersion(emptyList()) + } + + val (part1, part2) = from.parts + if (part1 is SemanticVersion.Companion.VersionPart.ReleasePart && + part2 is SemanticVersion.Companion.VersionPart.ReleasePart + ) { + return SemanticVersion(listOf(part1, part2)) + } + + return SemanticVersion(emptyList()) + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents.isNullOrEmpty()) { + reporter.error("Expected a parent") + return null + } + + if (!parents[0]!!.acceptsType(SemanticVersion::class.java)) { + reporter.error("First parent must produce a semantic version") + return null + } + + return ExtractVersionMajorMinorPropertyDerivation() + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt new file mode 100644 index 000000000..8d1aaf1dd --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty + +fun interface PreparedDerivation { + fun derive(parentValues: List): Any? +} + +interface PropertyDerivationFactory { + + fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? +} diff --git a/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt new file mode 100644 index 000000000..c762c09b5 --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt @@ -0,0 +1,74 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.HasMinecraftVersion +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion + +class RecommendJavaVersionForMcVersionPropertyDerivation(val default: Int) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val mcVersion: SemanticVersion = when (val version = parentValues[0]) { + is SemanticVersion -> version + is HasMinecraftVersion -> version.minecraftVersion + else -> return default + } + return MinecraftVersions.requiredJavaVersion(mcVersion).ordinal + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents.isNullOrEmpty()) { + reporter.error("Expected one parent") + return null + } + + if (parents.size > 1) { + reporter.warn("More than one parent defined") + } + + val parentValue = parents[0]!! + if (!parentValue.acceptsType(SemanticVersion::class.java) && + !parentValue.acceptsType(HasMinecraftVersion::class.java) + ) { + reporter.error("Parent must produce a semantic version or a value that has a Minecraft version") + return null + } + + val default = (derivation.default as? Number)?.toInt() + if (default == null) { + reporter.error("Default value is required and must be an integer") + return null + } + + return RecommendJavaVersionForMcVersionPropertyDerivation(default) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt new file mode 100644 index 000000000..c3688f9ad --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt @@ -0,0 +1,94 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty + +class ReplacePropertyDerivation( + val regex: Regex, + val replacement: String, + val maxLength: Int?, +) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val projectName = parentValues.first() as? String + ?: return null + + val sanitized = projectName.lowercase().replace(regex, replacement) + if (maxLength != null && sanitized.length > maxLength) { + return sanitized.substring(0, maxLength) + } + + return sanitized + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (derivation.parameters == null) { + reporter.error("Missing parameters") + return null + } + + if (parents.isNullOrEmpty()) { + reporter.error("Missing parent value") + return null + } + + if (parents.size > 2) { + reporter.warn("More than one parent defined") + } + + if (!parents[0]!!.acceptsType(String::class.java)) { + reporter.error("Parent property must produce a string value") + return null + } + + val regexString = derivation.parameters["regex"] as? String + if (regexString == null) { + reporter.error("Missing 'regex' string parameter") + return null + } + + val regex = try { + Regex(regexString) + } catch (t: Throwable) { + reporter.error("Invalid regex: '$regexString': ${t.message}") + return null + } + + val replacement = derivation.parameters["replacement"] as? String + if (replacement == null) { + reporter.error("Missing 'replacement' string parameter") + return null + } + + val maxLength = (derivation.parameters["maxLength"] as? Number)?.toInt() + return ReplacePropertyDerivation(regex, replacement, maxLength) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt new file mode 100644 index 000000000..88444bb5d --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt @@ -0,0 +1,67 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.PropertyDerivationSelect +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.thisLogger + +class SelectPropertyDerivation( + val parents: List?, + val options: List, + val default: Any?, +) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val properties = if (!parents.isNullOrEmpty()) { + parentValues.mapIndexed { i, value -> parents[i] to value }.toMap() + } else { + emptyMap() + } + for (option in options) { + if (TemplateEvaluator.condition(properties, option.condition).getOrLogException(thisLogger()) == true) { + return option.value + } + } + + return default + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (derivation.select == null) { + reporter.error("Missing select options") + return null + } + + return SelectPropertyDerivation(derivation.parents, derivation.select, derivation.default) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt new file mode 100644 index 000000000..8362a7b4a --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt @@ -0,0 +1,68 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.BuildSystemCoordinates +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.capitalize +import com.demonwav.mcdev.util.decapitalize + +class SuggestClassNamePropertyDerivation : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val coords = parentValues[0] as BuildSystemCoordinates + val name = parentValues[1] as String + return ClassFqn("${coords.groupId}.${name.decapitalize()}.${name.capitalize()}") + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents == null || parents.size < 2) { + reporter.error("Expected 2 parents") + return null + } + + if (parents.size > 2) { + reporter.warn("More than two parents defined") + } + + if (!parents[0]!!.acceptsType(BuildSystemCoordinates::class.java)) { + reporter.error("First parent must produce a build system coordinates") + return null + } + + if (!parents[1]!!.acceptsType(String::class.java)) { + reporter.error("Second parent must produce a string value") + return null + } + + return SuggestClassNamePropertyDerivation() + } + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt new file mode 100644 index 000000000..a65e225b7 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt @@ -0,0 +1,121 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.finalizers + +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.TemplateValidationReporterImpl +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute + +interface CreatorFinalizer { + + fun validate(reporter: TemplateValidationReporter, properties: Map) = Unit + + fun execute(context: WizardContext, properties: Map, templateProperties: Map) + + companion object { + private val EP_NAME = + ExtensionPointName.create("com.demonwav.minecraft-dev.creatorFinalizer") + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun validateAll( + reporter: TemplateValidationReporterImpl, + finalizers: List>, + ) { + for ((index, properties) in finalizers.withIndex()) { + reporter.subject = "Finalizer #$index" + + val type = properties["type"] as? String + if (type == null) { + reporter.error("Missing required 'type' value") + } + + val condition = properties["condition"] + if (condition != null && condition !is String) { + reporter.error("'condition' must be a string") + } + + if (type != null) { + val finalizer = COLLECTOR.findSingle(type) + if (finalizer == null) { + reporter.error("Unknown finalizer of type $type") + } else { + try { + finalizer.validate(reporter, properties) + } catch (t: Throwable) { + reporter.error("Unexpected error during finalizer validation: ${t.message}") + thisLogger().error("Unexpected error during finalizer validation", t) + } + } + } + } + } + + fun executeAll( + context: WizardContext, + finalizers: List>, + templateProperties: Map + ) { + for ((index, properties) in finalizers.withIndex()) { + val type = properties["type"] as String + val condition = properties["condition"] as? String + if (condition != null && + !TemplateEvaluator.condition(templateProperties, condition).getOrElse { false } + ) { + continue + } + + val finalizer = COLLECTOR.findSingle(type)!! + try { + finalizer.execute(context, properties, templateProperties) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + thisLogger().error("Unhandled exception in finalizer #$index ($type)", t) + } + } + } + } +} + +class CreatorFinalizerBean : BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("type") + @RequiredElement + lateinit var type: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getKey(): String? = type + + override fun getImplementationClassName(): String? = implementation +} diff --git a/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt new file mode 100644 index 000000000..ea099c9f8 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.finalizers + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.util.ExecUtil +import com.intellij.ide.util.projectWizard.WizardContext + +class GitAddAllFinalizer : CreatorFinalizer { + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + ExecUtil.execAndGetOutput(GeneralCommandLine("git", "add", ".").withWorkDirectory(context.projectFileDirectory)) + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt new file mode 100644 index 000000000..7385d945e --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt @@ -0,0 +1,40 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.finalizers + +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import org.jetbrains.plugins.gradle.service.project.open.canLinkAndRefreshGradleProject +import org.jetbrains.plugins.gradle.service.project.open.linkAndRefreshGradleProject + +class ImportGradleProjectFinalizer : CreatorFinalizer { + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + val project = context.project!! + val projectDir = context.projectFileDirectory + val canLink = canLinkAndRefreshGradleProject(projectDir, project, showValidationDialog = false) + thisLogger().info("canLink = $canLink projectDir = $projectDir") + if (canLink) { + linkAndRefreshGradleProject(projectDir, project) + thisLogger().info("Linking done") + } + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt new file mode 100644 index 000000000..fb0652c57 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt @@ -0,0 +1,57 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.finalizers + +import com.demonwav.mcdev.util.invokeAndWait +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.vfs.VfsUtil +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import org.jetbrains.idea.maven.project.importing.MavenImportingManager + +class ImportMavenProjectFinalizer : CreatorFinalizer { + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + val project = context.project!! + val projectDir = context.projectFileDirectory + + val pomFile = VfsUtil.findFile(Path.of(projectDir).resolve("pom.xml"), true) + ?: return + thisLogger().info("Invoking import on EDT pomFile = ${pomFile.path}") + val promise = invokeAndWait { + if (project.isDisposed || !project.isInitialized) { + return@invokeAndWait null + } + + MavenImportingManager.getInstance(project).linkAndImportFile(pomFile) + } + + if (promise == null) { + thisLogger().info("Could not start import") + return + } + + thisLogger().info("Waiting for import to finish") + promise.finishPromise.blockingGet(Int.MAX_VALUE, TimeUnit.SECONDS) + thisLogger().info("Import finished") + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt new file mode 100644 index 000000000..9d919d1f9 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt @@ -0,0 +1,54 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.finalizers + +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.util.runGradleTaskAndWait +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger + +class RunGradleTasksFinalizer : CreatorFinalizer { + + override fun validate( + reporter: TemplateValidationReporter, + properties: Map + ) { + @Suppress("UNCHECKED_CAST") + val tasks = properties["tasks"] as? List + if (tasks == null) { + reporter.warn("Missing list of 'tasks' to execute") + } + } + + override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + @Suppress("UNCHECKED_CAST") + val tasks = properties["tasks"] as List + val project = context.project!! + val projectDir = context.projectDirectory + + thisLogger().info("tasks = $tasks projectDir = $projectDir") + runGradleTaskAndWait(project, projectDir) { settings -> + settings.taskNames = tasks + } + + thisLogger().info("Done running tasks") + } +} diff --git a/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt b/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt new file mode 100644 index 000000000..d77d9e22e --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ArchitecturyVersionsModel( + val minecraft: SemanticVersion, + val forge: SemanticVersion?, + val neoforge: SemanticVersion?, + val loom: SemanticVersion, + val loader: SemanticVersion, + val yarn: FabricVersions.YarnVersion, + val useFabricApi: Boolean, + val fabricApi: SemanticVersion, + val useOfficialMappings: Boolean, + val useArchitecturyApi: Boolean, + val architecturyApi: SemanticVersion, +) : HasMinecraftVersion { + + override val minecraftVersion: SemanticVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + + val hasForge: Boolean by lazy { !forge?.parts.isNullOrEmpty() } + val forgeSpec: String? by lazy { forge?.parts?.getOrNull(0)?.versionString } + + val hasNeoforge: Boolean by lazy { !neoforge?.parts.isNullOrEmpty() } + val neoforgeSpec: String? by lazy { neoforge?.parts?.getOrNull(0)?.versionString } +} diff --git a/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt b/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt new file mode 100644 index 000000000..0eeab3c6e --- /dev/null +++ b/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt @@ -0,0 +1,27 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +@TemplateApi +data class BuildSystemCoordinates(val groupId: String, val artifactId: String, val version: String) { + + override fun toString(): String = "$groupId:$artifactId:$version" +} diff --git a/src/main/kotlin/creator/custom/model/ClassFqn.kt b/src/main/kotlin/creator/custom/model/ClassFqn.kt new file mode 100644 index 000000000..5383f3fac --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ClassFqn.kt @@ -0,0 +1,51 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +@TemplateApi +data class ClassFqn(val fqn: String) { + + /** + * The [Class.simpleName] of this class. + */ + val className by lazy { fqn.substringAfterLast('.') } + + /** + * The relative filesystem path to this class, without extension. + */ + val path by lazy { fqn.replace('.', '/') } + + /** + * The package name of this FQN as it would appear in source code. + */ + val packageName by lazy { fqn.substringBeforeLast('.') } + + /** + * The package path of this FQN reflected as a local filesystem path + */ + val packagePath by lazy { packageName.replace('.', '/') } + + fun withClassName(className: String) = copy("$packageName.$className") + + fun withSubPackage(name: String) = copy("$packageName.$name.$className") + + override fun toString(): String = fqn +} diff --git a/src/main/kotlin/creator/custom/model/CreatorJdk.kt b/src/main/kotlin/creator/custom/model/CreatorJdk.kt new file mode 100644 index 000000000..1e442b19b --- /dev/null +++ b/src/main/kotlin/creator/custom/model/CreatorJdk.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import com.intellij.openapi.projectRoots.JavaSdk +import com.intellij.openapi.projectRoots.Sdk + +@TemplateApi +data class CreatorJdk(val sdk: Sdk?) { + + val javaVersion: Int + get() = sdk?.let { JavaSdk.getInstance().getVersion(it)?.ordinal } ?: 17 +} diff --git a/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt b/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt new file mode 100644 index 000000000..c5111c7c5 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt @@ -0,0 +1,35 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class FabricVersionsModel( + override val minecraftVersion: SemanticVersion, + val loom: SemanticVersion, + val loader: SemanticVersion, + val yarn: FabricVersions.YarnVersion, + val useFabricApi: Boolean, + val fabricApi: SemanticVersion, + val useOfficialMappings: Boolean, +) : HasMinecraftVersion diff --git a/src/main/kotlin/creator/custom/model/ForgeVersions.kt b/src/main/kotlin/creator/custom/model/ForgeVersions.kt new file mode 100644 index 000000000..a308f4787 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ForgeVersions.kt @@ -0,0 +1,44 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ForgeVersions( + val minecraft: SemanticVersion, + val forge: SemanticVersion, +) : HasMinecraftVersion { + override val minecraftVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + val forgeSpec by lazy { forge.parts[0].versionString } +} diff --git a/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt b/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt new file mode 100644 index 000000000..c33cd9676 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt @@ -0,0 +1,29 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +interface HasMinecraftVersion { + + val minecraftVersion: SemanticVersion +} diff --git a/src/main/kotlin/creator/custom/model/LicenseData.kt b/src/main/kotlin/creator/custom/model/LicenseData.kt new file mode 100644 index 000000000..ddbf7932c --- /dev/null +++ b/src/main/kotlin/creator/custom/model/LicenseData.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import java.time.ZonedDateTime + +@TemplateApi +data class LicenseData( + val id: String, + val name: String, + val year: String = ZonedDateTime.now().year.toString(), +) diff --git a/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt b/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt new file mode 100644 index 000000000..c5a9bb2a2 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt @@ -0,0 +1,46 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class NeoForgeVersions( + val minecraft: SemanticVersion, + val neoforge: SemanticVersion, + val neogradle: SemanticVersion, + val moddev: SemanticVersion, +) : HasMinecraftVersion { + override val minecraftVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + val neoforgeSpec by lazy { neoforge.parts[0].versionString } +} diff --git a/src/main/kotlin/creator/custom/model/ParchmentVersions.kt b/src/main/kotlin/creator/custom/model/ParchmentVersions.kt new file mode 100644 index 000000000..0d11a3c74 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ParchmentVersions.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ParchmentVersions( + val use: Boolean, + val version: SemanticVersion, + override val minecraftVersion: SemanticVersion, + val includeOlderMcVersions: Boolean, + val includeSnapshots: Boolean, +) : HasMinecraftVersion diff --git a/src/main/kotlin/creator/custom/model/StringList.kt b/src/main/kotlin/creator/custom/model/StringList.kt new file mode 100644 index 000000000..d2b3bf09c --- /dev/null +++ b/src/main/kotlin/creator/custom/model/StringList.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +@TemplateApi +data class StringList(val values: List) : List by values { + + override fun toString(): String = values.joinToString() + + @JvmOverloads + fun toString(separator: String, prefix: String = "", postfix: String = ""): String = + values.joinToString(separator, prefix, postfix) +} diff --git a/src/main/kotlin/creator/custom/model/TemplateApi.kt b/src/main/kotlin/creator/custom/model/TemplateApi.kt new file mode 100644 index 000000000..fb053db2d --- /dev/null +++ b/src/main/kotlin/creator/custom/model/TemplateApi.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.model + +/** + * Marker annotation indicating classes exposed to templates. + * + * Be careful of not breaking source or binary compatibility of those APIs without a good reason. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class TemplateApi diff --git a/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt new file mode 100644 index 000000000..1bc3bc9b6 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt @@ -0,0 +1,92 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.update.PluginUtil +import com.demonwav.mcdev.util.refreshSync +import com.demonwav.mcdev.util.virtualFile +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class BuiltinTemplateProvider : RemoteTemplateProvider() { + + private val builtinRepoUrl = "https://github.com/minecraft-dev/templates/archive/refs/heads/v\$version.zip" + private val builtinTemplatesPath = PluginUtil.plugin.pluginPath.resolve("lib/resources/builtin-templates") + private val builtinTemplatesInnerPath = "templates-${TemplateDescriptor.FORMAT_VERSION}" + private var repoUpdated: Boolean = false + + override val label: String = MCDevBundle("template.provider.builtin.label") + + override val hasConfig: Boolean = true + + override fun init(indicator: ProgressIndicator, repos: List) { + if (repoUpdated || repos.none { it.data.toBoolean() }) { + // Auto update is disabled + return + } + + if (doUpdateRepo(indicator, label, builtinRepoUrl)) { + repoUpdated = true + } + } + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val remoteTemplates = doLoadTemplates(context, repo, builtinTemplatesInnerPath) + if (remoteTemplates.isNotEmpty()) { + return remoteTemplates + } + + val repoRoot = builtinTemplatesPath.virtualFile + ?: return emptyList() + repoRoot.refreshSync(context.modalityState) + return TemplateProvider.findTemplates(context.modalityState, repoRoot) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("BuiltinTemplateProvider config") + val autoUpdateProperty = propertyGraph.property(data.toBooleanStrictOrNull() != false) + + return panel { + row { + checkBox(MCDevBundle("creator.ui.custom.remote.auto_update.label")) + .bindSelected(autoUpdateProperty) + } + + onApply { + dataSetter(autoUpdateProperty.get().toString()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt new file mode 100644 index 000000000..6f0cd2f45 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt @@ -0,0 +1,40 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor + +/** + * Placeholder template + */ +object EmptyLoadedTemplate : LoadedTemplate { + + override val label: String = "Empty template" + override val tooltip: String = "Empty template tooltip" + + override val descriptor: TemplateDescriptor + get() = throw UnsupportedOperationException("The empty template can't have a descriptor") + + override val isValid: Boolean = false + + override fun loadTemplateContents(path: String): String? = + throw UnsupportedOperationException("The empty template can't have contents") +} diff --git a/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt new file mode 100644 index 000000000..186d58f40 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt @@ -0,0 +1,33 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor + +interface LoadedTemplate { + + val label: String + val tooltip: String? + val descriptor: TemplateDescriptor + val isValid: Boolean + + fun loadTemplateContents(path: String): String? +} diff --git a/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt new file mode 100644 index 000000000..d08fb037c --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt @@ -0,0 +1,94 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.refreshSync +import com.demonwav.mcdev.util.virtualFile +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.absolute + +class LocalTemplateProvider : TemplateProvider { + + override val label: String = MCDevBundle("template.provider.local.label") + + override val hasConfig: Boolean = true + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val rootPath = Path.of(repo.data.trim()).absolute() + val repoRoot = rootPath.virtualFile + ?: return emptyList() + val modalityState = context.modalityState + repoRoot.refreshSync(modalityState) + return TemplateProvider.findTemplates(modalityState, repoRoot) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("LocalTemplateProvider config") + val pathProperty = propertyGraph.property(data) + return panel { + row(MCDevBundle("creator.ui.custom.path.label")) { + val pathChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor().apply { + description = MCDevBundle("creator.ui.custom.path.dialog.description") + } + textFieldWithBrowseButton( + MCDevBundle("creator.ui.custom.path.dialog.title"), + null, + pathChooserDescriptor + ).align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(pathProperty) + .textValidation( + validationErrorIf(MCDevBundle("creator.validation.custom.path_not_a_directory")) { value -> + val file = kotlin.runCatching { + VirtualFileManager.getInstance().findFileByNioPath(Path.of(value)) + }.getOrNull() + file == null || !file.isDirectory + } + ) + } + + onApply { + dataSetter(pathProperty.get()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt new file mode 100644 index 000000000..2cbc70f16 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt @@ -0,0 +1,228 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.creator.selectProxy +import com.demonwav.mcdev.update.PluginUtil +import com.demonwav.mcdev.util.refreshSync +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.result.getOrNull +import com.github.kittinunf.result.onError +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.trim +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import com.intellij.util.io.createDirectories +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.writeBytes + +open class RemoteTemplateProvider : TemplateProvider { + + private var updatedTemplates = mutableSetOf() + + override val label: String = MCDevBundle("template.provider.remote.label") + + override val hasConfig: Boolean = true + + override fun init(indicator: ProgressIndicator, repos: List) { + for (repo in repos) { + ProgressManager.checkCanceled() + val remote = RemoteTemplateRepo.deserialize(repo.data) + ?: continue + if (!remote.autoUpdate || remote.url in updatedTemplates) { + continue + } + + if (doUpdateRepo(indicator, repo.name, remote.url)) { + updatedTemplates.add(remote.url) + } + } + } + + protected fun doUpdateRepo( + indicator: ProgressIndicator, + repoName: String, + originalRepoUrl: String + ): Boolean { + indicator.text2 = "Updating remote repository $repoName" + + val repoUrl = replaceVariables(originalRepoUrl) + + val manager = FuelManager() + manager.proxy = selectProxy(repoUrl) + val (_, _, result) = manager.get(repoUrl) + .header("User-Agent", "github_org/minecraft-dev/${PluginUtil.pluginVersion}") + .header("Accepts", "application/json") + .timeout(10000) + .response() + + val data = result.onError { + thisLogger().warn("Could not fetch remote templates repository update at $repoUrl", it) + }.getOrNull() ?: return false + + try { + val zipPath = RemoteTemplateRepo.getDestinationZip(repoName) + zipPath.parent.createDirectories() + zipPath.writeBytes(data) + + thisLogger().info("Remote templates repository update applied successfully") + return true + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + thisLogger().error("Failed to apply remote templates repository update of $repoName", t) + } + return false + } + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val remoteRepo = RemoteTemplateRepo.deserialize(repo.data) + ?: return emptyList() + return doLoadTemplates(context, repo, remoteRepo.innerPath) + } + + protected fun doLoadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo, + rawInnerPath: String + ): List { + val remoteRootPath = RemoteTemplateRepo.getDestinationZip(repo.name) + if (!remoteRootPath.exists()) { + return emptyList() + } + + val archiveRoot = remoteRootPath.absolutePathString() + JarFileSystem.JAR_SEPARATOR + + val fs = JarFileSystem.getInstance() + val rootFile = fs.refreshAndFindFileByPath(archiveRoot) + ?: return emptyList() + val modalityState = context.modalityState + rootFile.refreshSync(modalityState) + + val innerPath = replaceVariables(rawInnerPath) + val repoRoot = if (innerPath.isNotBlank()) { + rootFile.findFileByRelativePath(innerPath) + } else { + rootFile + } + + if (repoRoot == null) { + return emptyList() + } + + return TemplateProvider.findTemplates(modalityState, repoRoot) + } + + private fun replaceVariables(originalRepoUrl: String): String = + originalRepoUrl.replace("\$version", TemplateDescriptor.FORMAT_VERSION.toString()) + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("RemoteTemplateProvider config") + val defaultRepo = RemoteTemplateRepo.deserialize(data) + val urlProperty = propertyGraph.property(defaultRepo?.url ?: "").trim() + val autoUpdateProperty = propertyGraph.property(defaultRepo?.autoUpdate != false) + val innerPathProperty = propertyGraph.property(defaultRepo?.innerPath ?: "").trim() + + return panel { + row(MCDevBundle("creator.ui.custom.remote.url.label")) { + textField() + .comment(MCDevBundle("creator.ui.custom.remote.url.comment")) + .align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(urlProperty) + .textValidation(BuiltinValidations.nonBlank) + } + + row(MCDevBundle("creator.ui.custom.remote.inner_path.label")) { + textField() + .comment(MCDevBundle("creator.ui.custom.remote.inner_path.comment")) + .align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(innerPathProperty) + } + + row { + checkBox(MCDevBundle("creator.ui.custom.remote.auto_update.label")) + .bindSelected(autoUpdateProperty) + } + + onApply { + val repo = RemoteTemplateRepo(urlProperty.get(), autoUpdateProperty.get(), innerPathProperty.get()) + dataSetter(repo.serialize()) + } + } + } + + data class RemoteTemplateRepo(val url: String, val autoUpdate: Boolean, val innerPath: String) { + + fun serialize(): String = "$url\n$autoUpdate\n$innerPath" + + companion object { + + val templatesBaseDir: Path + get() = PathManager.getSystemDir().resolve("mcdev-templates") + + fun getDestinationZip(repoName: String): Path { + return templatesBaseDir.resolve("$repoName.zip") + } + + fun deserialize(data: String): RemoteTemplateRepo? { + if (data.isBlank()) { + return null + } + + val lines = data.lines() + return RemoteTemplateRepo( + lines[0], + lines.getOrNull(1).toBoolean(), + lines.getOrNull(2) ?: "", + ) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/TemplateProvider.kt b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt new file mode 100644 index 000000000..30efddbb2 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt @@ -0,0 +1,226 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.custom.TemplateResourceBundle +import com.demonwav.mcdev.util.fromJson +import com.demonwav.mcdev.util.refreshSync +import com.google.gson.Gson +import com.intellij.DynamicBundle +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileVisitor +import com.intellij.openapi.vfs.isFile +import com.intellij.openapi.vfs.readText +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute +import java.util.ResourceBundle +import javax.swing.JComponent + +/** + * Extensions responsible for creating a [TemplateDescriptor] based on whatever data it is provided in its configuration + * [UI][setupConfigUi]. + */ +interface TemplateProvider { + + val label: String + + val hasConfig: Boolean + + fun init(indicator: ProgressIndicator, repos: List) = Unit + + fun loadTemplates(context: WizardContext, repo: MinecraftSettings.TemplateRepo): Collection + + fun setupConfigUi(data: String, dataSetter: (String) -> Unit): JComponent? + + companion object { + + private val EP_NAME = + ExtensionPointName("com.demonwav.minecraft-dev.creatorTemplateProvider") + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun get(key: String): TemplateProvider? = COLLECTOR.findSingle(key) + + fun getAllKeys() = EP_NAME.extensionList.mapNotNull { it.key } + + fun findTemplates( + modalityState: ModalityState, + repoRoot: VirtualFile, + templates: MutableList = mutableListOf(), + bundle: ResourceBundle? = loadMessagesBundle(modalityState, repoRoot) + ): List { + val visitor = object : VirtualFileVisitor() { + override fun visitFile(file: VirtualFile): Boolean { + if (!file.isFile || !file.name.endsWith(".mcdev.template.json")) { + return true + } + + try { + createVfsLoadedTemplate(modalityState, file.parent, file, bundle = bundle) + ?.let(templates::add) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + val attachment = runCatching { Attachment(file.name, file.readText()) }.getOrNull() + if (attachment != null) { + thisLogger().error("Failed to load template ${file.path}", t, attachment) + } else { + thisLogger().error("Failed to load template ${file.path}", t) + } + } + + return true + } + } + VfsUtilCore.visitChildrenRecursively(repoRoot, visitor) + return templates + } + + fun loadMessagesBundle(modalityState: ModalityState, repoRoot: VirtualFile): ResourceBundle? = try { + val locale = DynamicBundle.getLocale() + // Simplified bundle resolution, but covers all the most common cases + val baseBundle = doLoadMessageBundle( + repoRoot.findChild("messages.properties"), + modalityState, + null + ) + val languageBundle = doLoadMessageBundle( + repoRoot.findChild("messages_${locale.language}.properties"), + modalityState, + baseBundle + ) + doLoadMessageBundle( + repoRoot.findChild("messages_${locale.language}_${locale.country}.properties"), + modalityState, + languageBundle + ) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to load resource bundle of template repository ${repoRoot.path}", t) + null + } + + private fun doLoadMessageBundle( + file: VirtualFile?, + modalityState: ModalityState, + parent: ResourceBundle? + ): ResourceBundle? { + if (file == null) { + return parent + } + + try { + return file.refreshSync(modalityState) + ?.inputStream?.reader()?.use { TemplateResourceBundle(it, parent) } + } catch (t: Throwable) { + if (t is ControlFlowException) { + return parent + } + + thisLogger().error("Failed to load resource bundle ${file.path}", t) + } + + return parent + } + + fun createVfsLoadedTemplate( + modalityState: ModalityState, + templateRoot: VirtualFile, + descriptorFile: VirtualFile, + tooltip: String? = null, + bundle: ResourceBundle? = null + ): VfsLoadedTemplate? { + descriptorFile.refreshSync(modalityState) + var descriptor = Gson().fromJson(descriptorFile.readText()) + if (descriptor.version != TemplateDescriptor.FORMAT_VERSION) { + thisLogger().warn("Cannot handle template ${descriptorFile.path} of version ${descriptor.version}") + return null + } + + if (descriptor.hidden == true) { + return null + } + + descriptor.bundle = bundle + + val labelKey = descriptor.label + ?: descriptorFile.name.removeSuffix(".mcdev.template.json").takeIf(String::isNotBlank) + ?: templateRoot.presentableName + val label = + descriptor.translateOrNull("platform.${labelKey.lowercase()}.label") ?: descriptor.translate(labelKey) + + if (descriptor.inherit != null) { + val parent = templateRoot.findFileByRelativePath(descriptor.inherit!!) + if (parent != null) { + parent.refresh(false, false) + val parentDescriptor = Gson().fromJson(parent.readText()) + val mergedProperties = parentDescriptor.properties.orEmpty() + descriptor.properties.orEmpty() + val mergedFiles = parentDescriptor.files.orEmpty() + descriptor.files.orEmpty() + descriptor = descriptor.copy(properties = mergedProperties, files = mergedFiles) + } else { + thisLogger().error( + "Could not find inherited template descriptor ${descriptor.inherit} from ${descriptorFile.path}" + ) + } + } + + if (bundle != null) { + descriptor.properties?.forEach { property -> + property.bundle = bundle + } + } + + return VfsLoadedTemplate(templateRoot, label, tooltip, descriptor, true) + } + } +} + +class TemplateProviderBean : BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("key") + @RequiredElement + lateinit var name: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getKey(): String? = name + + override fun getImplementationClassName(): String? = implementation +} diff --git a/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt new file mode 100644 index 000000000..aa34b456c --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt @@ -0,0 +1,43 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.readText +import java.io.FileNotFoundException + +class VfsLoadedTemplate( + val templateRoot: VirtualFile, + override val label: String, + override val tooltip: String? = null, + override val descriptor: TemplateDescriptor, + override val isValid: Boolean, +) : LoadedTemplate { + + override fun loadTemplateContents(path: String): String? { + templateRoot.refresh(false, true) + val virtualFile = templateRoot.findFileByRelativePath(path) + ?: throw FileNotFoundException("Could not find file $path in template root ${templateRoot.path}") + virtualFile.refresh(false, false) + return virtualFile.readText() + } +} diff --git a/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt new file mode 100644 index 000000000..6cae4d3ce --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt @@ -0,0 +1,92 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.refreshSync +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.isRegularFile + +class ZipTemplateProvider : TemplateProvider { + + override val label: String = MCDevBundle("template.provider.zip.label") + + override val hasConfig: Boolean = true + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val archiveRoot = repo.data + JarFileSystem.JAR_SEPARATOR + val fs = JarFileSystem.getInstance() + val rootFile = fs.refreshAndFindFileByPath(archiveRoot) + ?: return emptyList() + val modalityState = context.modalityState + rootFile.refreshSync(modalityState) + return TemplateProvider.findTemplates(modalityState, rootFile) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent { + val propertyGraph = PropertyGraph("ZipTemplateProvider config") + val pathProperty = propertyGraph.property(data) + + return panel { + row(MCDevBundle("creator.ui.custom.path.label")) { + val pathChooserDescriptor = FileChooserDescriptorFactory.createSingleLocalFileDescriptor() + .withFileFilter { it.extension == "zip" } + .apply { description = MCDevBundle("creator.ui.custom.archive.dialog.description") } + textFieldWithBrowseButton( + MCDevBundle("creator.ui.custom.archive.dialog.title"), + null, + pathChooserDescriptor + ).align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(pathProperty) + .textValidation( + validationErrorIf(MCDevBundle("creator.validation.custom.path_not_a_file")) { value -> + runCatching { !Path.of(value).isRegularFile() }.getOrDefault(true) + } + ) + } + + onApply { + dataSetter(pathProperty.get()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt new file mode 100644 index 000000000..1c742e80e --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt @@ -0,0 +1,481 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.MCDevBundle.invoke +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.ArchitecturyVersionsModel +import com.demonwav.mcdev.platform.architectury.ArchitecturyVersion +import com.demonwav.mcdev.platform.fabric.util.FabricApiVersions +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.platform.forge.version.ForgeVersion +import com.demonwav.mcdev.platform.neoforge.version.NeoForgeVersion +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class ArchitecturyVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ArchitecturyVersionsModel::class.java) { + + private val emptyVersion = SemanticVersion.release() + private val emptyValue = ArchitecturyVersionsModel( + emptyVersion, + emptyVersion, + emptyVersion, + emptyVersion, + emptyVersion, + FabricVersions.YarnVersion("", -1), + true, + emptyVersion, + true, + true, + emptyVersion, + ) + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var model: ArchitecturyVersionsModel by graphProperty + + val mcVersionProperty = graphProperty.transform({ it.minecraft }, { model.copy(minecraft = it) }) + val mcVersionModel = DefaultComboBoxModel() + + val forgeVersionProperty = graphProperty.transform({ it.forge }, { model.copy(forge = it) }) + val forgeVersionsModel = DefaultComboBoxModel() + val isForgeAvailableProperty = forgeVersionProperty.transform { !it?.parts.isNullOrEmpty() } + + val nfVersionProperty = graphProperty.transform({ it.neoforge }, { model.copy(neoforge = it) }) + val nfVersionsModel = DefaultComboBoxModel() + val isNfAvailableProperty = nfVersionProperty.transform { !it?.parts.isNullOrEmpty() } + + val loomVersionProperty = graphProperty.transform({ it.loom }, { model.copy(loom = it) }) + val loomVersionModel = DefaultComboBoxModel() + + val loaderVersionProperty = graphProperty.transform({ it.loader }, { model.copy(loader = it) }) + val loaderVersionModel = DefaultComboBoxModel() + + val yarnVersionProperty = graphProperty.transform({ it.yarn }, { model.copy(yarn = it) }) + val yarnVersionModel = DefaultComboBoxModel() + val yarnHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val versions = fabricVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + versions.mappings.any { it.gameVersion == mcVersionString } + } + + val fabricApiVersionProperty = graphProperty.transform({ it.fabricApi }, { model.copy(fabricApi = it) }) + val fabricApiVersionModel = DefaultComboBoxModel() + val useFabricApiVersionProperty = graphProperty.transform({ it.useFabricApi }, { model.copy(useFabricApi = it) }) + val fabricApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = fabricApiVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + apiVersions.versions.any { mcVersionString in it.gameVersions } + } + + val useOfficialMappingsProperty = + graphProperty.transform({ it.useOfficialMappings }, { model.copy(useOfficialMappings = it) }) + + val architecturyApiVersionProperty = + graphProperty.transform({ it.architecturyApi }, { model.copy(architecturyApi = it) }) + val architecturyApiVersionModel = DefaultComboBoxModel() + val useArchitecturyApiVersionProperty = + graphProperty.transform({ it.useArchitecturyApi }, { model.copy(useArchitecturyApi = it) }) + val architecturyApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = architecturyVersions + ?: return@transform true + apiVersions.versions.containsKey(mcVersion) + } + + override fun createDefaultValue(raw: Any?): ArchitecturyVersionsModel = when (raw) { + is String -> deserialize(raw) + else -> emptyValue + } + + override fun serialize(value: ArchitecturyVersionsModel): String { + return "${value.minecraft} ${value.forge} ${value.neoforge} ${value.loom} ${value.loader} ${value.yarn}" + + " ${value.useFabricApi} ${value.fabricApi} ${value.useOfficialMappings} ${value.useArchitecturyApi}" + + " ${value.architecturyApi}" + } + + override fun deserialize(string: String): ArchitecturyVersionsModel { + val segments = string.split(' ') + val yarnSegments = segments.getOrNull(5)?.split(':') + val yarnVersion = if (yarnSegments != null && yarnSegments.size == 2) { + FabricVersions.YarnVersion(yarnSegments[0], yarnSegments[1].toInt()) + } else { + emptyValue.yarn + } + return ArchitecturyVersionsModel( + segments.getOrNull(0)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(3)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(4)?.let(SemanticVersion::tryParse) ?: emptyVersion, + yarnVersion, + segments.getOrNull(6)?.toBoolean() != false, + segments.getOrNull(7)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(8)?.toBoolean() != false, + segments.getOrNull(9)?.toBoolean() != false, + segments.getOrNull(10)?.let(SemanticVersion::tryParse) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("ArchitecturyVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row("Minecraft Version:") { + comboBox(mcVersionModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row("Forge Version:") { + comboBox(forgeVersionsModel) + .bindItem(forgeVersionProperty) + .enabledIf(isForgeAvailableProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + .component + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row("NeoForge Version:") { + comboBox(nfVersionsModel) + .bindItem(nfVersionProperty) + .enabledIf(isNfAvailableProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + .component + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + // + // panel.row("Loom Version:") { + // comboBox(loomVersionModel) + // .bindItem(loomVersionProperty) + // .validationOnInput(BuiltinValidations.nonEmptyVersion) + // .validationOnApply(BuiltinValidations.nonEmptyVersion) + // .also { ComboboxSpeedSearch.installOn(it.component) } + // }.enabled(descriptor.editable != false) + + panel.row("Loader Version:") { + comboBox(loaderVersionModel) + .bindItem(loaderVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + // Official mappings forced currently, yarn mappings are not handled yet + // panel.row("Yarn Version:") { + // comboBox(yarnVersionModel) + // .bindItem(yarnVersionProperty) + // .enabledIf(useOfficialMappingsProperty.not()) + // .validationOnInput(BuiltinValidations.nonEmptyYarnVersion) + // .validationOnApply(BuiltinValidations.nonEmptyYarnVersion) + // .also { ComboboxSpeedSearch.installOn(it.component) } + // + // checkBox("Use official mappings") + // .bindSelected(useOfficialMappingsProperty) + // + // label("Unable to match Yarn versions to Minecraft version") + // .visibleIf(yarnHasMatchingGameVersion.not()) + // .component.foreground = JBColor.YELLOW + // }.enabled(descriptor.editable != false) + + panel.row("Fabric API Version:") { + comboBox(fabricApiVersionModel) + .bindItem(fabricApiVersionProperty) + .enabledIf(useFabricApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox("Use Fabric API") + .bindSelected(useFabricApiVersionProperty) + label("Unable to match API versions to Minecraft version") + .visibleIf(fabricApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.visibleIf(!loadingVersionsProperty) + + panel.row("Architectury API Version:") { + comboBox(architecturyApiVersionModel) + .bindItem(architecturyApiVersionProperty) + .enabledIf(useArchitecturyApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox("Use Architectury API") + .bindSelected(useArchitecturyApiVersionProperty) + label("Unable to match API versions to Minecraft version") + .visibleIf(architecturyApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (previousMcVersion == mcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + + updateForgeVersions() + updateNeoForgeVersions() + updateYarnVersions() + updateFabricApiVersions() + updateArchitecturyApiVersions() + } + + downloadVersions { + val fabricVersions = fabricVersions + if (fabricVersions != null) { + loaderVersionModel.removeAllElements() + loaderVersionModel.addAll(fabricVersions.loader) + loaderVersionProperty.set(fabricVersions.loader.firstOrNull() ?: emptyVersion) + } + + val loomVersions = loomVersions + if (loomVersions != null) { + loomVersionModel.removeAllElements() + loomVersionModel.addAll(loomVersions) + val defaultValue = loomVersions.find { + it.parts.any { it is SemanticVersion.Companion.VersionPart.PreReleasePart } + } ?: loomVersions.firstOrNull() ?: emptyVersion + + loomVersionProperty.set(defaultValue) + } + + updateMcVersionsList() + + loadingVersionsProperty.set(false) + } + } + + private fun updateMcVersionsList() { + val architecturyVersions = architecturyVersions + ?: return + + val mcVersions = architecturyVersions.versions.keys.sortedDescending() + mcVersionModel.removeAllElements() + mcVersionModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + } + + private fun updateForgeVersions() { + val mcVersion = mcVersionProperty.get() + + val filterExpr = descriptor.parameters?.get("forgeMcVersionFilter") as? String + if (filterExpr != null) { + val conditionProps = mapOf("MC_VERSION" to mcVersion) + if (!TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true)) { + forgeVersionsModel.removeAllElements() + application.invokeLater { + // For some reason we have to set those properties later for the values to actually be set + // and the enabled state to be set appropriately + forgeVersionProperty.set(null) + } + return + } + } + + val availableForgeVersions = forgeVersions!!.getForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + forgeVersionsModel.removeAllElements() + forgeVersionsModel.addAll(availableForgeVersions) + application.invokeLater { + forgeVersionProperty.set(availableForgeVersions.firstOrNull()) + } + } + + private fun updateNeoForgeVersions() { + val mcVersion = mcVersionProperty.get() + + val filterExpr = descriptor.parameters?.get("neoForgeMcVersionFilter") as? String + if (filterExpr != null) { + val conditionProps = mapOf("MC_VERSION" to mcVersion) + if (!TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true)) { + nfVersionsModel.removeAllElements() + application.invokeLater { + nfVersionProperty.set(null) + } + return + } + } + + val availableNeoForgeVersions = neoForgeVersions!!.getNeoForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + nfVersionsModel.removeAllElements() + nfVersionsModel.addAll(availableNeoForgeVersions) + application.invokeLater { + nfVersionProperty.set(availableNeoForgeVersions.firstOrNull()) + } + } + + private fun updateYarnVersions() { + val fabricVersions = fabricVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val yarnVersions = if (yarnHasMatchingGameVersion.get()) { + fabricVersions.mappings.asSequence() + .filter { it.gameVersion == mcVersionString } + .map { it.version } + .toList() + } else { + fabricVersions.mappings.map { it.version } + } + yarnVersionModel.removeAllElements() + yarnVersionModel.addAll(yarnVersions) + yarnVersionProperty.set(yarnVersions.firstOrNull() ?: emptyValue.yarn) + } + + private fun updateFabricApiVersions() { + val fabricApiVersions = fabricApiVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val apiVersions = if (fabricApiHasMatchingGameVersion.get()) { + fabricApiVersions.versions.asSequence() + .filter { mcVersionString in it.gameVersions } + .map(FabricApiVersions.Version::version) + .toList() + } else { + fabricApiVersions.versions.map(FabricApiVersions.Version::version) + } + fabricApiVersionModel.removeAllElements() + fabricApiVersionModel.addAll(apiVersions) + fabricApiVersionProperty.set(apiVersions.firstOrNull() ?: emptyVersion) + } + + private fun updateArchitecturyApiVersions() { + val architecturyVersions = architecturyVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val availableArchitecturyApiVersions = architecturyVersions.getArchitecturyVersions(mcVersion) + architecturyApiVersionModel.removeAllElements() + architecturyApiVersionModel.addAll(availableArchitecturyApiVersions) + + architecturyApiVersionProperty.set(availableArchitecturyApiVersions.firstOrNull() ?: emptyVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var forgeVersions: ForgeVersion? = null + private var neoForgeVersions: NeoForgeVersion? = null + private var fabricVersions: FabricVersions? = null + private var loomVersions: List? = null + private var fabricApiVersions: FabricApiVersions? = null + private var architecturyVersions: ArchitecturyVersion? = null + + private fun downloadVersions(completeCallback: () -> Unit) { + if (hasDownloadedVersions) { + completeCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + awaitAll( + asyncIO { ForgeVersion.downloadData().also { forgeVersions = it } }, + asyncIO { NeoForgeVersion.downloadData().also { neoForgeVersions = it } }, + asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, + asyncIO { + collectMavenVersions( + "https://maven.architectury.dev/dev/architectury/architectury-loom/maven-metadata.xml" + ).also { + loomVersions = it + .mapNotNull(SemanticVersion::tryParse) + .sortedDescending() + } + }, + asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, + asyncIO { ArchitecturyVersion.downloadData().also { architecturyVersions = it } }, + ) + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + completeCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ArchitecturyVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt new file mode 100644 index 000000000..57072d519 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt @@ -0,0 +1,67 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.intellij.icons.AllIcons +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.content.AlertIcon +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindSelected + +class BooleanCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, Boolean::class.java) { + + override fun createDefaultValue(raw: Any?): Boolean = raw as? Boolean ?: false + + override fun serialize(value: Boolean): String = value.toString() + + override fun deserialize(string: String): Boolean = string.toBoolean() + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + val label = descriptor.translatedLabel + panel.row(label) { + val warning = descriptor.translatedWarning + if (warning != null) { + icon(AlertIcon(AllIcons.General.Warning)) + .gap(RightGap.SMALL) + .comment(descriptor.translate(warning)) + } + + this.checkBox(label.removeSuffix(":").trim()) + .bindSelected(graphProperty) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = BooleanCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt new file mode 100644 index 000000000..2d70ef5cc --- /dev/null +++ b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt @@ -0,0 +1,135 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.BuildSystemCoordinates +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.ui.validation.CHECK_ARTIFACT_ID +import com.intellij.openapi.ui.validation.CHECK_GROUP_ID +import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY +import com.intellij.openapi.ui.validation.WHEN_GRAPH_PROPAGATION_FINISHED +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +private val nonExampleValidation = validationErrorIf(MCDevBundle("creator.validation.group_id_non_example")) { + it == "org.example" +} + +class BuildSystemCoordinatesCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, BuildSystemCoordinates::class.java) { + + private val default = createDefaultValue(descriptor.default) + + override val graphProperty: GraphProperty = graph.property(default) + var coords: BuildSystemCoordinates by graphProperty + + private val groupIdProperty = graphProperty.transform({ it.groupId }, { coords.copy(groupId = it) }) + private val artifactIdProperty = graphProperty.transform({ it.artifactId }, { coords.copy(artifactId = it) }) + private val versionProperty = graphProperty.transform({ it.version }, { coords.copy(version = it) }) + + override fun createDefaultValue(raw: Any?): BuildSystemCoordinates { + val str = (raw as? String) ?: return createDefaultValue() + return deserialize(str) + } + + private fun createDefaultValue() = BuildSystemCoordinates("org.example", "", "1.0-SNAPSHOT") + + override fun serialize(value: BuildSystemCoordinates): String = + "${value.groupId}:${value.artifactId}:${value.version}" + + override fun deserialize(string: String): BuildSystemCoordinates { + val segments = string.split(':') + + val groupId = segments.getOrElse(0) { "" } + val artifactId = segments.getOrElse(1) { "" } + val version = segments.getOrElse(2) { "" } + return BuildSystemCoordinates(groupId, artifactId, version) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val projectNameProperty = properties["PROJECT_NAME"]?.graphProperty + if (projectNameProperty != null) { + val projectName = projectNameProperty.get() + if (projectName is String) { + coords = coords.copy(artifactId = projectName) + } + + graphProperty.dependsOn(projectNameProperty, false) { + val newProjectName = projectNameProperty.get() + if (newProjectName is String) { + coords.copy(artifactId = newProjectName) + } else { + coords + } + } + } + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.collapsibleGroup(MCDevBundle("creator.ui.group.title")) { + this.row(MCDevBundle("creator.ui.group.group_id")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.groupIdProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(CHECK_NON_EMPTY, CHECK_GROUP_ID, nonExampleValidation) + } + this.row(MCDevBundle("creator.ui.group.artifact_id")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.artifactIdProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(CHECK_NON_EMPTY, CHECK_ARTIFACT_ID) + } + this.row(MCDevBundle("creator.ui.group.version")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.versionProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(BuiltinValidations.validVersion) + } + }.expanded = true + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = BuildSystemCoordinatesCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt new file mode 100644 index 000000000..5ee2470ad --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt @@ -0,0 +1,78 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.SuggestClassNamePropertyDerivation +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +class ClassFqnCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, ClassFqn::class.java) { + + override fun createDefaultValue(raw: Any?): ClassFqn = ClassFqn(raw as? String ?: "") + + override fun serialize(value: ClassFqn): String = value.toString() + + override fun deserialize(string: String): ClassFqn = ClassFqn(string) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@ClassFqnCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .textValidation(BuiltinValidations.validClassFqn) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "suggestClassName" -> { + val parents = collectDerivationParents(reporter) + SuggestClassNamePropertyDerivation.create(reporter, parents, derives) + } + + else -> null + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ClassFqnCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/CreatorProperty.kt b/src/main/kotlin/creator/custom/types/CreatorProperty.kt new file mode 100644 index 000000000..3e9e1845c --- /dev/null +++ b/src/main/kotlin/creator/custom/types/CreatorProperty.kt @@ -0,0 +1,288 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.bindStorage +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row + +abstract class CreatorProperty( + val descriptor: TemplatePropertyDescriptor, + val graph: PropertyGraph, + protected val properties: Map>, + val valueType: Class +) { + private var derivation: PreparedDerivation? = null + private lateinit var visibleProperty: GraphProperty + + abstract val graphProperty: GraphProperty + + abstract fun createDefaultValue(raw: Any?): T + + abstract fun serialize(value: T): String + + abstract fun deserialize(string: String): T + + open fun toStringProperty(graphProperty: GraphProperty): ObservableMutableProperty = + graphProperty.transform(::serialize, ::deserialize) + + open fun get(): T? { + val value = graphProperty.get() + if (descriptor.nullIfDefault == true) { + val default = createDefaultValue(descriptor.default) + if (value == default) { + return null + } + } + + return value + } + + fun acceptsType(type: Class<*>): Boolean = type.isAssignableFrom(valueType) + + /** + * Produces a new value based on the provided [parentValues] and the template-defined [derivation] configuration. + * + * You must **NOT** [set][GraphProperty.set] the value of [graphProperty] in the process. You may however [get][GraphProperty.get] it at will. + * + * @param parentValues the values of the properties this [graphProperty] depends on + * @param derivation the configuration of the desired derivation + * + * @see GraphProperty.dependsOn + */ + open fun derive(parentValues: List?, derivation: PropertyDerivation): Any? { + if (this.derivation == null) { + throw IllegalStateException("This property has not been configured with a derivation") + } + + val result = this.derivation!!.derive(parentValues.orEmpty()) + if (this.derivation is SelectPropertyDerivation) { + return convertSelectDerivationResult(result) + } + + return result + } + + protected open fun convertSelectDerivationResult(original: Any?): Any? = original + + abstract fun buildUi(panel: Panel, context: WizardContext) + + /** + * Prepares everything this property needs, like calling [GraphProperty]'s [GraphProperty.afterChange] and + * [GraphProperty.dependsOn] on this property or other properties declared before this one. + * + * [properties] contains all the properties declared in the descriptor + * up to this one, forward references are not permitted. + * + * This is also where you should validate the [descriptor] values you want to use, and report all validation errors + * or warnings through the [reporter], use [TemplateValidationReporter.fatal] if the error is a show-stopper and + * the validation cannot even proceed further. + */ + open fun setupProperty(reporter: TemplateValidationReporter) { + if (descriptor.remember != false && descriptor.derives == null) { + val storageKey = when (val remember = descriptor.remember) { + null, true -> makeStorageKey() + is String -> makeCustomStorageKey(remember) + else -> { + reporter.error("Invalid 'remember' value. Must be a boolean or a string") + null + } + } + + if (storageKey != null) { + toStringProperty(graphProperty).bindStorage(storageKey) + } + } + + visibleProperty = setupVisibleProperty(reporter, descriptor.visible) + + if (descriptor.derives != null) { + val parents = descriptor.derives.parents + ?: return reporter.error("No parents specified in derivation") + for (parent in parents) { + if (!properties.containsKey(parent)) { + return reporter.error("Unknown parent property '$parent' in derivation") + } + } + + derivation = setupDerivation(reporter, descriptor.derives) + if (derivation == null) { + reporter.fatal("Unknown method derivation: ${descriptor.derives}") + } + + @Suppress("UNCHECKED_CAST") + graphProperty.set(derive(collectDerivationParentValues(reporter), descriptor.derives) as T) + for (parent in parents) { + val parentProperty = properties[parent]!! + graphProperty.dependsOn(parentProperty.graphProperty, descriptor.derives.whenModified != false) { + @Suppress("UNCHECKED_CAST") + derive(collectDerivationParentValues(), descriptor.derives) as T + } + } + } + + if (descriptor.inheritFrom != null) { + val parentProperty = properties[descriptor.inheritFrom] + ?: return reporter.error("Unknown parent property '${descriptor.inheritFrom}' in derivation") + + @Suppress("UNCHECKED_CAST") + graphProperty.set(parentProperty.graphProperty.get() as T) + graphProperty.dependsOn(parentProperty.graphProperty, true) { + @Suppress("UNCHECKED_CAST") + parentProperty.graphProperty.get() as T + } + } + } + + protected open fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = null + + protected fun makeStorageKey(discriminator: String? = null): String { + val base = "${javaClass.name}.property.${descriptor.name}.${descriptor.type}" + if (discriminator == null) { + return base + } + + return "$base.$discriminator" + } + + protected fun makeCustomStorageKey(key: String): String { + return "${javaClass.name}.property.$key" + } + + protected fun collectPropertiesValues(names: List? = null): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + return if (names == null) { + properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } else { + names.associateWithTo(mutableMapOf()) { properties[it]?.get() } + } + } + + protected fun collectDerivationParents(reporter: TemplateValidationReporter? = null): List?>? = + descriptor.derives?.parents?.map { parentName -> + val property = properties[parentName] + if (property == null) { + reporter?.error("Unknown parent property: $parentName") + } + return@map property + } + + protected fun collectDerivationParentValues(reporter: TemplateValidationReporter? = null): List? = + descriptor.derives?.parents?.map { parentName -> + val property = properties[parentName] + if (property == null) { + reporter?.error("Unknown parent property: $parentName") + } + return@map property?.get() + } + + protected fun Row.propertyVisibility(): Row = this.visibleIf(visibleProperty) + + private fun setupVisibleProperty( + reporter: TemplateValidationReporter, + visibility: Any? + ): GraphProperty { + val prop = graph.property(true) + if (visibility == null || visibility is Boolean) { + prop.set(visibility != false) + return prop + } + + if (visibility !is Map<*, *>) { + reporter.error("Visibility can only be a boolean or an object") + return prop + } + + var dependsOn = visibility["dependsOn"] + if (dependsOn !is String && (dependsOn !is List<*> || dependsOn.any { it !is String })) { + reporter.error( + "Expected 'visible' to have a 'dependsOn' value that is either a string or a list of strings" + ) + return prop + } + + val dependenciesNames = when (dependsOn) { + is String -> setOf(dependsOn) + is Collection<*> -> dependsOn.filterIsInstance().toSet() + else -> throw IllegalStateException("Should not be reached") + } + val dependencies = dependenciesNames.mapNotNull { + val dependency = this.properties[it] + if (dependency == null) { + reporter.error("Visibility dependency '$it' does not exist") + } + dependency + } + if (dependencies.size != dependenciesNames.size) { + // Errors have already been reported + return prop + } + + val condition = visibility["condition"] + if (condition !is String) { + reporter.error("Expected 'visible' to have a 'condition' string") + return prop + } + + var didInitialUpdate = false + val update: () -> Boolean = { + val conditionProperties = dependencies.associate { prop -> prop.descriptor.name to prop.get() } + val result = TemplateEvaluator.condition(conditionProperties, condition) + val exception = result.exceptionOrNull() + if (exception != null) { + if (!didInitialUpdate) { + didInitialUpdate = true + reporter.error("Failed to compute initial visibility: ${exception.message}") + thisLogger().info("Failed to compute initial visibility: ${exception.message}", exception) + } else { + thisLogger().error("Failed to compute initial visibility: ${exception.message}", exception) + } + } + + result.getOrDefault(true) + } + + prop.set(update()) + for (dependency in dependencies) { + prop.dependsOn(dependency.graphProperty, deleteWhenModified = false, update) + } + + return prop + } +} diff --git a/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt new file mode 100644 index 000000000..8d3689d50 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt @@ -0,0 +1,73 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute + +interface CreatorPropertyFactory { + + companion object { + + private val EP_NAME = ExtensionPointName>( + "com.demonwav.minecraft-dev.creatorPropertyType" + ) + + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun createFromType( + type: String, + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*>? { + return COLLECTOR.findSingle(type)?.create(descriptor, graph, properties) + } + } + + fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> +} + +class CreatorPropertyFactoryBean : + BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("type") + @RequiredElement + lateinit var type: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getImplementationClassName(): String = implementation + + override fun getKey(): String = type +} diff --git a/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt new file mode 100644 index 000000000..b51b0e58c --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt @@ -0,0 +1,50 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.Panel + +class ExternalCreatorProperty( + descriptor: TemplatePropertyDescriptor = TemplatePropertyDescriptor("", "", "", default = ""), + graph: PropertyGraph, + properties: Map>, + override val graphProperty: GraphProperty, + valueType: Class, +) : CreatorProperty(descriptor, graph, properties, valueType) { + + override fun setupProperty(reporter: TemplateValidationReporter) = Unit + + override fun createDefaultValue(raw: Any?): T = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun serialize(value: T): String = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun deserialize(string: String): T = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun buildUi(panel: Panel, context: WizardContext) = Unit +} diff --git a/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt new file mode 100644 index 000000000..870c470cb --- /dev/null +++ b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt @@ -0,0 +1,350 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.FabricVersionsModel +import com.demonwav.mcdev.platform.fabric.util.FabricApiVersions +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.bindBooleanStorage +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.ui.validation.WHEN_GRAPH_PROPAGATION_FINISHED +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class FabricVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, FabricVersionsModel::class.java) { + + private val emptyVersion = SemanticVersion.release() + private val emptyValue = FabricVersionsModel( + emptyVersion, + emptyVersion, + emptyVersion, + FabricVersions.YarnVersion("", -1), + true, + emptyVersion, + false, + ) + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var model: FabricVersionsModel by graphProperty + + val mcVersionProperty = graphProperty.transform({ it.minecraftVersion }, { model.copy(minecraftVersion = it) }) + val mcVersionModel = DefaultComboBoxModel() + val showMcSnapshotsProperty = graph.property(false) + .bindBooleanStorage(makeStorageKey("showMcSnapshots")) + + val loomVersionProperty = graphProperty.transform({ it.loom }, { model.copy(loom = it) }) + val loomVersionModel = DefaultComboBoxModel() + + val loaderVersionProperty = graphProperty.transform({ it.loader }, { model.copy(loader = it) }) + val loaderVersionModel = DefaultComboBoxModel() + + val yarnVersionProperty = graphProperty.transform({ it.yarn }, { model.copy(yarn = it) }) + val yarnVersionModel = DefaultComboBoxModel() + val yarnHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val versions = fabricVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + versions.mappings.any { it.gameVersion == mcVersionString } + } + + val fabricApiVersionProperty = graphProperty.transform({ it.fabricApi }, { model.copy(fabricApi = it) }) + val fabricApiVersionModel = DefaultComboBoxModel() + val useFabricApiVersionProperty = graphProperty.transform({ it.useFabricApi }, { model.copy(useFabricApi = it) }) + val fabricApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = fabricApiVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + apiVersions.versions.any { mcVersionString in it.gameVersions } + } + + val useOfficialMappingsProperty = + graphProperty.transform({ it.useOfficialMappings }, { model.copy(useOfficialMappings = it) }) + + override fun createDefaultValue(raw: Any?): FabricVersionsModel = when (raw) { + is String -> deserialize(raw) + else -> emptyValue + } + + override fun serialize(value: FabricVersionsModel): String { + return "${value.minecraftVersion} ${value.loom} ${value.loader} ${value.yarn}" + + " ${value.useFabricApi} ${value.fabricApi} ${value.useOfficialMappings}" + } + + override fun deserialize(string: String): FabricVersionsModel { + val segments = string.split(' ') + val yarnSegments = segments.getOrNull(3)?.split(':') + val yarnVersion = if (yarnSegments != null && yarnSegments.size == 2) { + FabricVersions.YarnVersion(yarnSegments[0], yarnSegments[1].toInt()) + } else { + emptyValue.yarn + } + return FabricVersionsModel( + segments.getOrNull(0)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + yarnVersion, + segments.getOrNull(4).toBoolean(), + segments.getOrNull(5)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(6).toBoolean(), + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("FabricVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionModel) + .bindItem(mcVersionProperty) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.show_snapshots.label")) + .bindSelected(showMcSnapshotsProperty) + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.loom_version.label")) { + comboBox(loomVersionModel) + .bindItem(loomVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.loader_version.label")) { + comboBox(loaderVersionModel) + .bindItem(loaderVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.yarn_version.label")) { + comboBox(yarnVersionModel) + .bindItem(yarnVersionProperty) + .enabledIf(useOfficialMappingsProperty.not()) + .validationOnInput(BuiltinValidations.nonEmptyYarnVersion) + .validationOnApply(BuiltinValidations.nonEmptyYarnVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.use_official_mappings.label")) + .bindSelected(useOfficialMappingsProperty) + + label(MCDevBundle("creator.ui.warn.no_yarn_to_mc_match")) + .visibleIf(yarnHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.fabricapi_version.label")) { + comboBox(fabricApiVersionModel) + .bindItem(fabricApiVersionProperty) + .enabledIf(useFabricApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.use_fabricapi.label")) + .bindSelected(useFabricApiVersionProperty) + label(MCDevBundle("creator.ui.warn.no_fabricapi_to_mc_match")) + .visibleIf(fabricApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + showMcSnapshotsProperty.afterChange { updateMcVersionsList() } + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (previousMcVersion == mcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + updateYarnVersions() + updateFabricApiVersions() + } + + downloadVersion { + val fabricVersions = fabricVersions + if (fabricVersions != null) { + loaderVersionModel.removeAllElements() + loaderVersionModel.addAll(fabricVersions.loader) + loaderVersionProperty.set(fabricVersions.loader.firstOrNull() ?: emptyVersion) + + updateMcVersionsList() + } + + val loomVersions = loomVersions + if (loomVersions != null) { + loomVersionModel.removeAllElements() + loomVersionModel.addAll(loomVersions) + val defaultValue = loomVersions.firstOrNull { it.toString().endsWith("-SNAPSHOT") } + ?: loomVersions.firstOrNull() + ?: emptyVersion + + loomVersionProperty.set(defaultValue) + } + + loadingVersionsProperty.set(false) + } + } + + private fun updateMcVersionsList() { + val versions = fabricVersions + ?: return + + val showSnapshots = showMcSnapshotsProperty.get() + val mcVersions = versions.game.asSequence() + .filter { showSnapshots || it.stable } + .mapNotNull { version -> SemanticVersion.tryParse(version.version) } + .toList() + + mcVersionModel.removeAllElements() + mcVersionModel.addAll(mcVersions) + mcVersionProperty.set(mcVersions.firstOrNull() ?: emptyVersion) + } + + private fun updateYarnVersions() { + val fabricVersions = fabricVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val yarnVersions = if (yarnHasMatchingGameVersion.get()) { + fabricVersions.mappings.asSequence() + .filter { it.gameVersion == mcVersionString } + .map { it.version } + .toList() + } else { + fabricVersions.mappings.map { it.version } + } + yarnVersionModel.removeAllElements() + yarnVersionModel.addAll(yarnVersions) + yarnVersionProperty.set(yarnVersions.firstOrNull() ?: emptyValue.yarn) + } + + private fun updateFabricApiVersions() { + val fabricApiVersions = fabricApiVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val apiVersions = if (fabricApiHasMatchingGameVersion.get()) { + fabricApiVersions.versions.asSequence() + .filter { mcVersionString in it.gameVersions } + .map(FabricApiVersions.Version::version) + .toList() + } else { + fabricApiVersions.versions.map(FabricApiVersions.Version::version) + } + fabricApiVersionModel.removeAllElements() + fabricApiVersionModel.addAll(apiVersions) + fabricApiVersionProperty.set(apiVersions.firstOrNull() ?: emptyVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var fabricVersions: FabricVersions? = null + private var loomVersions: List? = null + private var fabricApiVersions: FabricApiVersions? = null + + private fun downloadVersion(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + awaitAll( + asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, + asyncIO { + collectMavenVersions( + "https://maven.fabricmc.net/net/fabricmc/fabric-loom/maven-metadata.xml" + ).mapNotNull(SemanticVersion::tryParse) + .sortedDescending() + .also { loomVersions = it } + }, + asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, + ) + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = FabricVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt new file mode 100644 index 000000000..de3464fae --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt @@ -0,0 +1,219 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.ForgeVersions +import com.demonwav.mcdev.platform.forge.version.ForgeVersion +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlin.collections.Map +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class ForgeVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ForgeVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: ForgeVersions by graphProperty + + private var previousMcVersion: SemanticVersion? = null + + private val mcVersionProperty = graphProperty.transform({ it.minecraft }, { versions.copy(minecraft = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val forgeVersionProperty = graphProperty.transform({ it.forge }, { versions.copy(forge = it) }) + private val forgeVersionsModel = DefaultComboBoxModel() + + private var mcVersionFilterParents: List? = null + + override fun createDefaultValue(raw: Any?): ForgeVersions { + if (raw is String) { + return deserialize(raw) + } + + return ForgeVersions(emptyVersion, emptyVersion) + } + + override fun serialize(value: ForgeVersions): String { + return "${value.minecraft} ${value.forge}" + } + + override fun deserialize(string: String): ForgeVersions { + val versions = string.split(' ') + .take(2) + .map { SemanticVersion.tryParse(it) ?: emptyVersion } + + return ForgeVersions( + versions.getOrNull(0) ?: emptyVersion, + versions.getOrNull(1) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("ForgeVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + label(MCDevBundle("creator.ui.forge_version.label")).gap(RightGap.SMALL) + comboBox(forgeVersionsModel) + .bindItem(forgeVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + val availableForgeVersions = forgeVersion!!.getForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + forgeVersionsModel.removeAllElements() + forgeVersionsModel.addAll(availableForgeVersions) + forgeVersionProperty.set(availableForgeVersions.firstOrNull() ?: emptyVersion) + } + + descriptor.parameters?.get("mcVersionFilterParents")?.let { parents -> + if (parents !is List<*> || parents.any { it !is String }) { + reporter.error("mcVersionFilterParents must be a list of strings") + } else { + @Suppress("UNCHECKED_CAST") + this.mcVersionFilterParents = parents as List + for (parent in parents) { + val parentProp = properties[parent] + if (parentProp == null) { + reporter.error("Unknown mcVersionFilter parent $parent") + continue + } + + parentProp.graphProperty.afterChange { + reloadMinecraftVersions() + } + } + } + } + + downloadVersions { + reloadMinecraftVersions() + + loadingVersionsProperty.set(false) + } + } + + private fun reloadMinecraftVersions() { + val forgeVersions = forgeVersion + ?: return + + val filterExpr = descriptor.parameters?.get("mcVersionFilter") as? String + val mcVersions = if (filterExpr != null) { + val conditionProps = collectPropertiesValues(mcVersionFilterParents) + forgeVersions.sortedMcVersions.filter { version -> + conditionProps["MC_VERSION"] = version + TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true) + } + } else { + forgeVersions.sortedMcVersions + } + + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var forgeVersion: ForgeVersion? = null + + private fun downloadVersions(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + forgeVersion = ForgeVersion.downloadData() + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ForgeVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt new file mode 100644 index 000000000..67e931edf --- /dev/null +++ b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt @@ -0,0 +1,62 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.model.StringList +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns + +class InlineStringListCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, StringList::class.java) { + + override fun createDefaultValue(raw: Any?): StringList = deserialize(raw as? String ?: "") + + override fun serialize(value: StringList): String = value.values.joinToString(transform = String::trim) + + override fun deserialize(string: String): StringList = string.split(',') + .map(String::trim) + .filter(String::isNotBlank) + .run(::StringList) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@InlineStringListCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = InlineStringListCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt new file mode 100644 index 000000000..bcd6edc6b --- /dev/null +++ b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt @@ -0,0 +1,82 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.RecommendJavaVersionForMcVersionPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindIntText +import com.intellij.ui.dsl.builder.columns + +class IntegerCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, Int::class.java) { + + override fun createDefaultValue(raw: Any?): Int = (raw as? Number)?.toInt() ?: 0 + + override fun serialize(value: Int): String = value.toString() + + override fun deserialize(string: String): Int = string.toIntOrNull() ?: 0 + + override fun convertSelectDerivationResult(original: Any?): Any? = (original as? Number)?.toInt() + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.intTextField().bindIntText(graphProperty) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "recommendJavaVersionForMcVersion" -> { + val parents = collectDerivationParents(reporter) + RecommendJavaVersionForMcVersionPropertyDerivation.create(reporter, parents, derives) + } + + null -> { + // No need to collect parent values for this one because it is not used + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = IntegerCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt new file mode 100644 index 000000000..02608ed5f --- /dev/null +++ b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt @@ -0,0 +1,77 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.JdkComboBoxWithPreference +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.model.CreatorJdk +import com.demonwav.mcdev.creator.jdkComboBoxWithPreference +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.projectRoots.JavaSdkVersion +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.ui.dsl.builder.Panel + +class JdkCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, CreatorJdk::class.java) { + + private lateinit var jdkComboBox: JdkComboBoxWithPreference + + override fun createDefaultValue(raw: Any?): CreatorJdk = CreatorJdk(null) + + override fun serialize(value: CreatorJdk): String = value.sdk?.homePath ?: "" + + override fun deserialize(string: String): CreatorJdk = + CreatorJdk(ProjectJdkTable.getInstance().allJdks.find { it.homePath == string }) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val sdkProperty = graphProperty.transform(CreatorJdk::sdk, ::CreatorJdk) + jdkComboBox = this.jdkComboBoxWithPreference(context, sdkProperty, descriptor.name).component + + val minVersionPropName = descriptor.default as? String + if (minVersionPropName != null) { + val minVersionProperty = properties[minVersionPropName] + ?: throw RuntimeException( + "Could not find property $minVersionPropName referenced" + + " by default value of property ${descriptor.name}" + ) + + jdkComboBox.setPreferredJdk(JavaSdkVersion.entries[minVersionProperty.graphProperty.get() as Int]) + minVersionProperty.graphProperty.afterPropagation { + jdkComboBox.setPreferredJdk(JavaSdkVersion.entries[minVersionProperty.graphProperty.get() as Int]) + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = JdkCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt new file mode 100644 index 000000000..fc6ae05df --- /dev/null +++ b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt @@ -0,0 +1,74 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.model.LicenseData +import com.demonwav.mcdev.util.License +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import java.time.ZonedDateTime + +class LicenseCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, LicenseData::class.java) { + + override val graphProperty: GraphProperty = + graph.property(createDefaultValue(descriptor.default)) + + override fun createDefaultValue(raw: Any?): LicenseData = + deserialize(raw as? String ?: License.ALL_RIGHTS_RESERVED.id) + + override fun serialize(value: LicenseData): String = value.id + + override fun deserialize(string: String): LicenseData = + LicenseData(string, License.byId(string)?.toString() ?: string, ZonedDateTime.now().year.toString()) + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val model = EnumComboBoxModel(License::class.java) + val licenseEnumProperty = graphProperty.transform( + { License.byId(it.id) ?: License.entries.first() }, + { deserialize(it.id) } + ) + comboBox(model) + .bindItem(licenseEnumProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = LicenseCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt new file mode 100644 index 000000000..733af37ae --- /dev/null +++ b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt @@ -0,0 +1,177 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class MavenArtifactVersionCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SemanticVersionCreatorProperty(descriptor, graph, properties) { + + lateinit var sourceUrl: String + var rawVersionFilter: (String) -> Boolean = { true } + var versionFilter: (SemanticVersion) -> Boolean = { true } + + override val graphProperty: GraphProperty = graph.property(SemanticVersion(emptyList())) + private val versionsProperty = graph.property>(emptyList()) + private val loadingVersionsProperty = graph.property(true) + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val combobox = comboBox(versionsProperty.get()) + .bindItem(graphProperty) + .enabled(descriptor.editable != false) + .also { ComboboxSpeedSearch.installOn(it.component) } + + cell(AsyncProcessIcon(makeStorageKey("progress"))) + .visibleIf(loadingVersionsProperty) + + versionsProperty.afterChange { versions -> + combobox.component.removeAllItems() + for (version in versions) { + combobox.component.addItem(version) + } + } + }.propertyVisibility() + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val url = descriptor.parameters?.get("sourceUrl") as? String + if (url == null) { + reporter.error("Expected string parameter 'sourceUrl'") + return + } + + sourceUrl = url + + val rawVersionFilterCondition = descriptor.parameters?.get("rawVersionFilter") + if (rawVersionFilterCondition != null) { + if (rawVersionFilterCondition !is String) { + reporter.error("'rawVersionFilter' must be a string") + } else { + rawVersionFilter = { version -> + val props = mapOf("version" to version) + TemplateEvaluator.condition(props, rawVersionFilterCondition) + .getOrLogException(thisLogger()) == true + } + } + } + + val versionFilterCondition = descriptor.parameters?.get("versionFilter") + if (versionFilterCondition != null) { + if (versionFilterCondition !is String) { + reporter.error("'versionFilter' must be a string") + } else { + versionFilter = { version -> + val props = mapOf("version" to version) + TemplateEvaluator.condition(props, versionFilterCondition) + .getOrLogException(thisLogger()) == true + } + } + } + + downloadVersions( + // The key might be a bit too unique, but that'll do the job + descriptor.name + "@" + descriptor.hashCode(), + sourceUrl, + rawVersionFilter, + versionFilter, + descriptor.limit ?: 50 + ) { versions -> + versionsProperty.set(versions) + loadingVersionsProperty.set(false) + } + } + + companion object { + + private var versionsCache = ConcurrentHashMap>() + + private fun downloadVersions( + key: String, + url: String, + rawVersionFilter: (String) -> Boolean, + versionFilter: (SemanticVersion) -> Boolean, + limit: Int, + uiCallback: (List) -> Unit + ) { + // Let's not mix up cached versions if different properties + // point to the same URL, but have different filters or limits + val cacheKey = "$key-$url" + val cachedVersions = versionsCache[cacheKey] + if (cachedVersions != null) { + uiCallback(cachedVersions) + return + } + + application.executeOnPooledThread { + runBlocking { + val versions = collectMavenVersions(url) + .asSequence() + .filter(rawVersionFilter) + .mapNotNull(SemanticVersion::tryParse) + .filter(versionFilter) + .sortedDescending() + .take(limit) + .toList() + + versionsCache[cacheKey] = versions + + withContext(Dispatchers.Swing) { + uiCallback(versions) + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = MavenArtifactVersionCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt new file mode 100644 index 000000000..10925897d --- /dev/null +++ b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt @@ -0,0 +1,211 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.NeoForgeVersions +import com.demonwav.mcdev.platform.neoforge.version.NeoForgeVersion +import com.demonwav.mcdev.platform.neoforge.version.NeoGradleVersion +import com.demonwav.mcdev.platform.neoforge.version.platform.neoforge.version.NeoModDevVersion +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class NeoForgeVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, NeoForgeVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: NeoForgeVersions by graphProperty + + private var previousMcVersion: SemanticVersion? = null + + private val mcVersionProperty = graphProperty.transform({ it.minecraft }, { versions.copy(minecraft = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val nfVersionProperty = graphProperty.transform({ it.neoforge }, { versions.copy(neoforge = it) }) + private val nfVersionsModel = DefaultComboBoxModel() + private val ngVersionProperty = graphProperty.transform({ it.neogradle }, { versions.copy(neogradle = it) }) + private val mdVersionProperty = graphProperty.transform({ it.moddev }, { versions.copy(moddev = it) }) + + override fun createDefaultValue(raw: Any?): NeoForgeVersions { + if (raw is String) { + return deserialize(raw) + } + + return NeoForgeVersions(emptyVersion, emptyVersion, emptyVersion, emptyVersion) + } + + override fun serialize(value: NeoForgeVersions): String { + return "${value.minecraft} ${value.neoforge} ${value.neogradle} ${value.moddev}" + } + + override fun deserialize(string: String): NeoForgeVersions { + val versions = string.split(' ') + .take(4) + .map { SemanticVersion.tryParse(it) ?: emptyVersion } + + return NeoForgeVersions( + versions.getOrNull(0) ?: emptyVersion, + versions.getOrNull(1) ?: emptyVersion, + versions.getOrNull(2) ?: emptyVersion, + versions.getOrNull(3) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("NeoForgeVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + label(MCDevBundle("creator.ui.neoforge_version.label")).gap(RightGap.SMALL) + comboBox(nfVersionsModel) + .bindItem(nfVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + val availableNfVersions = nfVersion!!.getNeoForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + nfVersionsModel.removeAllElements() + nfVersionsModel.addAll(availableNfVersions) + nfVersionProperty.set(availableNfVersions.firstOrNull() ?: emptyVersion) + } + + val mcVersionFilter = descriptor.parameters?.get("mcVersionFilter") as? String + downloadVersion(mcVersionFilter) { + val mcVersions = mcVersions ?: return@downloadVersion + + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + + ngVersionProperty.set(ngVersion?.versions?.firstOrNull() ?: emptyVersion) + mdVersionProperty.set(mdVersion?.versions?.firstOrNull() ?: emptyVersion) + + loadingVersionsProperty.set(false) + } + } + + companion object { + + private var hasDownloadedVersions = false + + private var nfVersion: NeoForgeVersion? = null + private var ngVersion: NeoGradleVersion? = null + private var mdVersion: NeoModDevVersion? = null + private var mcVersions: List? = null + + private fun downloadVersion(mcVersionFilter: String?, uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + awaitAll( + asyncIO { NeoForgeVersion.downloadData().also { nfVersion = it } }, + asyncIO { NeoGradleVersion.downloadData().also { ngVersion = it } }, + asyncIO { NeoModDevVersion.downloadData().also { mdVersion = it } }, + ) + + mcVersions = nfVersion?.sortedMcVersions?.let { mcVersion -> + if (mcVersionFilter != null) { + mcVersion.filter { version -> + val conditionProps = mapOf("MC_VERSION" to version) + TemplateEvaluator.condition(conditionProps, mcVersionFilter).getOrDefault(true) + } + } else { + mcVersion + } + } + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = NeoForgeVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt new file mode 100644 index 000000000..360c3d2f9 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt @@ -0,0 +1,281 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.ParchmentVersion +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.HasMinecraftVersion +import com.demonwav.mcdev.creator.custom.model.ParchmentVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.util.application +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class ParchmentCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ParchmentVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: ParchmentVersions by graphProperty + + private var availableParchmentVersions: List = emptyList() + + private val useParchmentProperty = graphProperty.transform({ it.use }, { versions.copy(use = it) }) + private val versionProperty = graphProperty.transform({ it.version }, { versions.copy(version = it) }) + private val versionsModel = DefaultComboBoxModel() + private val mcVersionProperty = + graphProperty.transform({ it.minecraftVersion }, { versions.copy(minecraftVersion = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val includeOlderMcVersionsProperty = + graphProperty.transform({ it.includeOlderMcVersions }, { versions.copy(includeOlderMcVersions = it) }) + private val includeSnapshotsProperty = + graphProperty.transform({ it.includeSnapshots }, { versions.copy(includeSnapshots = it) }) + + override fun createDefaultValue(raw: Any?): ParchmentVersions { + if (raw is String) { + return deserialize(raw) + } + + return ParchmentVersions(true, emptyVersion, emptyVersion, false, false) + } + + override fun serialize(value: ParchmentVersions): String { + return "${value.use} ${value.version} ${value.minecraftVersion}" + + " ${value.includeOlderMcVersions} ${value.includeSnapshots}" + } + + override fun deserialize(string: String): ParchmentVersions { + val segments = string.split(' ') + return ParchmentVersions( + segments.getOrNull(0)?.toBoolean() ?: true, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(3).toBoolean(), + segments.getOrNull(4).toBoolean(), + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + checkBox("Use Parchment") + .bindSelected(useParchmentProperty) + + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .enabledIf(useParchmentProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + comboBox(versionsModel) + .bindItem(versionProperty) + .enabledIf(useParchmentProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + + panel.row("Include") { + checkBox("Older Minecraft versions") + .bindSelected(includeOlderMcVersionsProperty) + .enabledIf(useParchmentProperty) + + checkBox("Snapshots") + .bindSelected(includeSnapshotsProperty) + .enabledIf(useParchmentProperty) + }.enabled(descriptor.editable != false) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val platformMcVersionPropertyName = descriptor.parameters?.get("minecraftVersionProperty") as? String + val platformMcVersionProperty = properties[platformMcVersionPropertyName] + if (platformMcVersionProperty != null) { + graphProperty.dependsOn(platformMcVersionProperty.graphProperty, true) { + val minecraftVersion = getPlatformMinecraftVersion() + if (minecraftVersion != null) { + graphProperty.get().copy(minecraftVersion = minecraftVersion) + } else { + graphProperty.get() + } + } + } + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + refreshVersionsLists(updateMcVersions = false) + } + + var previousOlderMcVersions: Boolean? = null + includeOlderMcVersionsProperty.afterChange { newValue -> + if (previousOlderMcVersions == newValue) { + return@afterChange + } + + previousOlderMcVersions = newValue + refreshVersionsLists() + } + + var previousIncludeSnapshots: Boolean? = null + includeSnapshotsProperty.afterChange { newValue -> + if (previousIncludeSnapshots == newValue) { + return@afterChange + } + + previousIncludeSnapshots = newValue + refreshVersionsLists() + } + + downloadVersions { + refreshVersionsLists() + + val minecraftVersion = getPlatformMinecraftVersion() + if (minecraftVersion != null) { + mcVersionProperty.set(minecraftVersion) + } + } + } + + private fun refreshVersionsLists(updateMcVersions: Boolean = true) { + val includeOlderMcVersions = includeOlderMcVersionsProperty.get() + val includeSnapshots = includeSnapshotsProperty.get() + + if (updateMcVersions) { + val platformMcVersion = getPlatformMinecraftVersion() + availableParchmentVersions = allParchmentVersions + ?.filter { version -> + if (!includeOlderMcVersions && platformMcVersion != null && version.mcVersion < platformMcVersion) { + return@filter false + } + + if (!includeSnapshots && version.parchmentVersion.contains("-SNAPSHOT")) { + return@filter false + } + + return@filter true + } + ?: return + + val mcVersions = availableParchmentVersions.mapTo(mutableSetOf(), ParchmentVersion::mcVersion) + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraftVersion in mcVersions -> defaultValue.minecraftVersion + else -> getPlatformMinecraftVersion() ?: mcVersions.first() + } + + if (mcVersionProperty.get() != selectedMcVersion) { + mcVersionProperty.set(selectedMcVersion) + } + } + + val selectedMcVersion = mcVersionProperty.get() + val parchmentVersions = availableParchmentVersions.asSequence() + .filter { it.mcVersion == selectedMcVersion } + .mapNotNull { SemanticVersion.tryParse(it.parchmentVersion) } + .sortedDescending() + .toList() + versionsModel.removeAllElements() + versionsModel.addAll(parchmentVersions) + versionProperty.set(parchmentVersions.firstOrNull() ?: emptyVersion) + } + + private fun getPlatformMinecraftVersion(): SemanticVersion? { + val platformMcVersionPropertyName = descriptor.parameters?.get("minecraftVersionProperty") as? String + val platformMcVersionProperty = properties[platformMcVersionPropertyName] + + val version = when (val version = platformMcVersionProperty?.get()) { + is SemanticVersion -> version + is HasMinecraftVersion -> version.minecraftVersion + else -> return null + } + + // Ensures we get no trailing .0 for the first major version (1.21.0 -> 1.21) + // This is required because otherwise those versions won't be properly compared against Parchment's + val normalizedVersion = version.parts.dropLastWhile { part -> + part is SemanticVersion.Companion.VersionPart.ReleasePart && part.version == 0 + } + + return SemanticVersion(normalizedVersion) + } + + companion object { + + private var hasDownloadedVersions = false + + private var allParchmentVersions: List? = null + + private fun downloadVersions(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + allParchmentVersions = ParchmentVersion.downloadData() + .sortedByDescending(ParchmentVersion::parchmentVersion) + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ParchmentCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt new file mode 100644 index 000000000..f500d03d0 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt @@ -0,0 +1,86 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.ExtractVersionMajorMinorPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_SHORT +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns + +open class SemanticVersionCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, SemanticVersion::class.java) { + + override fun createDefaultValue(raw: Any?): SemanticVersion = + SemanticVersion.tryParse(raw as? String ?: "") ?: SemanticVersion(emptyList()) + + override fun serialize(value: SemanticVersion): String = value.toString() + + override fun deserialize(string: String): SemanticVersion = + SemanticVersion.tryParse(string) ?: SemanticVersion(emptyList()) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@SemanticVersionCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_SHORT) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "extractVersionMajorMinor" -> { + val parents = collectDerivationParents(reporter) + ExtractVersionMajorMinorPropertyDerivation.create(reporter, parents, derives) + } + + null -> { + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + override fun convertSelectDerivationResult(original: Any?): Any? { + return (original as? String)?.let(SemanticVersion::tryParse) + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = SemanticVersionCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt new file mode 100644 index 000000000..7a735d7fb --- /dev/null +++ b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt @@ -0,0 +1,134 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.ComboboxSpeedSearch +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import java.awt.Component +import javax.swing.DefaultListCellRenderer +import javax.swing.JList + +abstract class SimpleCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map>, + valueType: Class +) : CreatorProperty(descriptor, graph, properties, valueType) { + + private val options: Map? = makeOptionsList() + + private fun makeOptionsList(): Map? { + val map = when (val options = descriptor.options) { + is Map<*, *> -> options.mapValues { descriptor.translate(it.value.toString()) } + is Iterable<*> -> options.associateWithTo(linkedMapOf()) { + val optionKey = it.toString() + descriptor.translateOrNull("creator.ui.${descriptor.name.lowercase()}.option.${optionKey.lowercase()}") + ?: optionKey + } + + else -> null + } + + return map?.mapKeys { + @Suppress("UNCHECKED_CAST") + when (val key = it.key) { + is String -> deserialize(key) + else -> key + } as T + } + } + + private val isDropdown = !options.isNullOrEmpty() + private val defaultValue by lazy { + val raw = if (isDropdown) { + if (descriptor.default is Number && descriptor.options is List<*>) { + descriptor.options[descriptor.default.toInt()] + } else { + descriptor.default ?: options?.keys?.firstOrNull() + } + } else { + descriptor.default + } + + createDefaultValue(raw) + } + + override val graphProperty: GraphProperty by lazy { graph.property(defaultValue) } + + override fun buildUi(panel: Panel, context: WizardContext) { + if (isDropdown) { + if (graphProperty.get() !in options!!.keys) { + graphProperty.set(defaultValue) + } + + panel.row(descriptor.translatedLabel) { + if (descriptor.forceDropdown == true) { + comboBox(options.keys, DropdownAutoRenderer()) + .bindItem(graphProperty) + .enabled(descriptor.editable != false) + .also { + val component = it.component + ComboboxSpeedSearch.installOn(component) + val validation = + BuiltinValidations.isAnyOf(component::getSelectedItem, options.keys, component) + it.validationOnInput(validation) + it.validationOnApply(validation) + } + } else { + segmentedButton(options.keys) { options[it] ?: it.toString() } + .bind(graphProperty) + .enabled(descriptor.editable != false) + .maxButtonsCount(4) + .validation { + val message = MCDevBundle("creator.validation.invalid_option") + addInputRule(message) { it.selectedItem !in options.keys } + addApplyRule(message) { it.selectedItem !in options.keys } + } + } + }.propertyVisibility() + } else { + buildSimpleUi(panel, context) + } + } + + abstract fun buildSimpleUi(panel: Panel, context: WizardContext) + + private inner class DropdownAutoRenderer : DefaultListCellRenderer() { + + override fun getListCellRendererComponent( + list: JList?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + val label = options!![value] ?: value.toString() + return super.getListCellRendererComponent(list, label, index, isSelected, cellHasFocus) + } + } +} diff --git a/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt new file mode 100644 index 000000000..31582bcc7 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt @@ -0,0 +1,103 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.ReplacePropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +class StringCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, String::class.java) { + + private var validationRegex: Regex? = null + + override fun createDefaultValue(raw: Any?): String = raw as? String ?: "" + + override fun serialize(value: String): String = value + + override fun deserialize(string: String): String = string + + override fun toStringProperty(graphProperty: GraphProperty) = graphProperty + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val regexString = descriptor.validator as? String + if (regexString != null) { + try { + validationRegex = regexString.toRegex() + } catch (t: Throwable) { + reporter.error("Invalid validator regex: '$regexString': ${t.message}") + } + } + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "replace" -> { + val parents = collectDerivationParents(reporter) + ReplacePropertyDerivation.create(reporter, parents, derives) + } + + null -> { + // No need to collect parent values for this one because it is not used + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val textField = textField().bindText(this@StringCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + if (validationRegex != null) { + textField.textValidation(BuiltinValidations.byRegex(validationRegex!!)) + } + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = StringCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/step/UseMixinsStep.kt b/src/main/kotlin/creator/step/UseMixinsStep.kt index e5b9b1c1b..c328712b5 100644 --- a/src/main/kotlin/creator/step/UseMixinsStep.kt +++ b/src/main/kotlin/creator/step/UseMixinsStep.kt @@ -36,7 +36,7 @@ class UseMixinsStep(parent: NewProjectWizardStep) : AbstractNewProjectWizardStep override fun setupUI(builder: Panel) { with(builder) { - row(MCDevBundle("creator.ui.mixins.label")) { + row(MCDevBundle("creator.ui.use_mixins.label")) { checkBox("") .bindSelected(useMixinsProperty) } diff --git a/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt b/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt index dae07b3d7..e5df5ce13 100644 --- a/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt +++ b/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt @@ -29,21 +29,13 @@ import com.github.kittinunf.fuel.core.requests.suspendable import com.github.kittinunf.fuel.coroutines.awaitString import com.google.gson.Gson import com.google.gson.annotations.SerializedName -import java.io.IOException class ArchitecturyVersion private constructor( val versions: Map>, ) { fun getArchitecturyVersions(mcVersion: SemanticVersion): List { - return try { - val architecturyVersions = versions[mcVersion] - ?: throw IOException("Could not find any architectury versions for $mcVersion") - architecturyVersions.take(50) - } catch (e: IOException) { - e.printStackTrace() - emptyList() - } + return versions[mcVersion].orEmpty().take(50) } data class ModrinthVersionApi( diff --git a/src/main/kotlin/platform/fabric/util/FabricVersions.kt b/src/main/kotlin/platform/fabric/util/FabricVersions.kt index 9e1a03167..7b898ad78 100644 --- a/src/main/kotlin/platform/fabric/util/FabricVersions.kt +++ b/src/main/kotlin/platform/fabric/util/FabricVersions.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.fabric.util +import com.demonwav.mcdev.creator.custom.model.TemplateApi import com.demonwav.mcdev.creator.selectProxy import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.SemanticVersion @@ -36,6 +37,7 @@ class FabricVersions(val game: List, val mappings: List, val loa class Game(val version: String, val stable: Boolean) class Mappings(val gameVersion: String, val version: YarnVersion) + @TemplateApi class YarnVersion(val name: String, val build: Int) : Comparable { override fun toString() = name override fun compareTo(other: YarnVersion) = build.compareTo(other.build) diff --git a/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt b/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt new file mode 100644 index 000000000..c6d8ac4d0 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt @@ -0,0 +1,50 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.neoforge.version.platform.neoforge.version + +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.openapi.diagnostic.logger +import java.io.IOException + +class NeoModDevVersion private constructor(val versions: List) { + + companion object { + private val LOGGER = logger() + + suspend fun downloadData(): NeoModDevVersion? { + try { + val url = "https://maven.neoforged.net/releases/net/neoforged/moddev" + + "/net.neoforged.moddev.gradle.plugin/maven-metadata.xml" + val versions = collectMavenVersions(url) + .asSequence() + .mapNotNull(SemanticVersion.Companion::tryParse) + .sortedDescending() + .take(50) + .toList() + return NeoModDevVersion(versions) + } catch (e: IOException) { + LOGGER.error("Failed to retrieve NeoForged ModDev version data", e) + } + return null + } + } +} diff --git a/src/main/kotlin/util/MinecraftVersions.kt b/src/main/kotlin/util/MinecraftVersions.kt index 9d19a575c..7333c5aa3 100644 --- a/src/main/kotlin/util/MinecraftVersions.kt +++ b/src/main/kotlin/util/MinecraftVersions.kt @@ -25,6 +25,7 @@ import com.intellij.openapi.projectRoots.JavaSdkVersion object MinecraftVersions { val MC1_12_2 = SemanticVersion.release(1, 12, 2) val MC1_14_4 = SemanticVersion.release(1, 14, 4) + val MC1_16 = SemanticVersion.release(1, 16) val MC1_16_1 = SemanticVersion.release(1, 16, 1) val MC1_16_5 = SemanticVersion.release(1, 16, 5) val MC1_17 = SemanticVersion.release(1, 17) @@ -35,6 +36,7 @@ object MinecraftVersions { val MC1_19_3 = SemanticVersion.release(1, 19, 3) val MC1_19_4 = SemanticVersion.release(1, 19, 4) val MC1_20 = SemanticVersion.release(1, 20) + val MC1_20_1 = SemanticVersion.release(1, 20, 1) val MC1_20_2 = SemanticVersion.release(1, 20, 2) val MC1_20_3 = SemanticVersion.release(1, 20, 3) val MC1_20_4 = SemanticVersion.release(1, 20, 4) diff --git a/src/main/kotlin/util/files.kt b/src/main/kotlin/util/files.kt index a1847d967..a00fd6c21 100644 --- a/src/main/kotlin/util/files.kt +++ b/src/main/kotlin/util/files.kt @@ -20,9 +20,11 @@ package com.demonwav.mcdev.util +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.RefreshQueue import java.io.File import java.io.IOException import java.nio.file.Path @@ -57,6 +59,7 @@ val VirtualFile.mcPath: String? operator fun Manifest.get(attribute: String): String? = mainAttributes.getValue(attribute) operator fun Manifest.get(attribute: Attributes.Name): String? = mainAttributes.getValue(attribute) -fun VirtualFile.refreshFs(): VirtualFile { - return this.parent.findOrCreateChildData(this, this.name) +fun VirtualFile.refreshSync(modalityState: ModalityState): VirtualFile? { + RefreshQueue.getInstance().refresh(false, this.isDirectory, null, modalityState, this) + return this.parent?.findOrCreateChildData(this, this.name) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e208da41b..28d579ca5 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -74,6 +74,16 @@ + + + + + + + + + + @@ -123,6 +133,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -182,6 +218,7 @@ + @@ -1227,5 +1264,8 @@ description="Copy the reference to clipboard in Access Widener format"> + diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 7ab481b10..2ee0ef24d 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -18,7 +18,7 @@ # along with this program. If not, see . # -creator.ui.build_system.label.generic=Build System: +creator.ui.build_system.label=Build System: creator.ui.build_system.label.gradle=Gradle creator.ui.build_system.label.maven=Maven @@ -31,12 +31,38 @@ creator.ui.platform.type.label=Platform Type: creator.ui.platform.label=Platform: creator.ui.platform.mod.name=Mod creator.ui.platform.plugin.name=Plugin +creator.ui.group.default.label=Default +creator.ui.group.mod.label=Mod +creator.ui.group.plugin.label=Plugin +creator.ui.group.proxy.label=Proxy + +creator.ui.custom.step.description=Creating project based on template... +creator.ui.custom.repos.label=Repositories: +creator.ui.custom.groups.label=Groups: +creator.ui.custom.templates.label=Templates: +creator.ui.custom.path.label=Templates Path: +creator.ui.custom.path.dialog.title=Template Root +creator.ui.custom.path.dialog.description=Select the root directory of the template repository +creator.ui.custom.archive.dialog.title=Template Archive +creator.ui.custom.archive.dialog.description=Select the ZIP file containing the template +creator.ui.custom.remote.url.label=Download URL: +creator.ui.custom.remote.url.comment='$version' will be replaced by the template descriptor version currently in use +creator.ui.custom.remote.inner_path.label=Inner Path: +creator.ui.custom.remote.inner_path.comment='$version' will be replaced by the template descriptor version currently in use +creator.ui.custom.remote.auto_update.label=Auto update + +creator.ui.warn.no_properties=This template has no properties +creator.ui.error.template_warns_and_errors=This template contains warnings and errors: +creator.ui.error.template_warns=This template contains warnings: +creator.ui.error.template_errors=This template contains errors: creator.ui.license.label=License: creator.ui.main_class.label=Main Class: -creator.ui.mc_version.label=Minecraft Version: -creator.ui.mod_name.label=Mod Name: -creator.ui.plugin_name.label=Plugin Name: +creator.ui.mc_version.label=Minecraft &Version: +creator.ui.mod_name.label=Mod &Name: +creator.ui.mod_id.label=Mod &ID: +creator.ui.plugin_name.label=Plugin &Name: +creator.ui.plugin_id.label=Plugin &ID: creator.ui.description.label=Description: creator.ui.authors.label=Authors: creator.ui.website.label=Website: @@ -44,13 +70,40 @@ creator.ui.repository.label=Repository: creator.ui.issue_tracker.label=Issue Tracker: creator.ui.update_url.label=Update URL: creator.ui.depend.label=Depend: +creator.ui.log_prefix.label=Log Prefix: +creator.ui.load_at.label=Load At: +creator.ui.load_at.option.startup=Startup: +creator.ui.load_at.option.postworld=Post World: creator.ui.soft_depend.label=Soft Depend: -creator.ui.mixins.label=Use Mixins: +creator.ui.use_mixins.label=Use &Mixins: +creator.ui.split_sources.label=Split Sources: +creator.ui.java_version.label=Java Version: +creator.ui.jdk.label=JDK: +creator.ui.optional_settings.label=Optional Settings creator.ui.parchment.label=Parchment: creator.ui.parchment.include.label=Include: creator.ui.parchment.include.old_mc.label=Older Minecraft versions creator.ui.parchment.include.snapshots.label=Snapshot versions creator.ui.parchment.no_version.message=No versions of Parchment matching your configuration +creator.ui.mod_environment.label=Environment: +creator.ui.mod_environment.option.*=Both +creator.ui.mod_environment.option.client=Client +creator.ui.mod_environment.option.server=Server +creator.ui.forge_version.label=Forge: +creator.ui.neoforge_version.label=NeoForge: +creator.ui.show_snapshots.label=Show snapshots: +creator.ui.loom_version.label=Loom Version: +creator.ui.loader_version.label=Loader Version: +creator.ui.yarn_version.label=Yarn Version: +creator.ui.use_official_mappings.label=Use official mappings +creator.ui.fabricapi_version.label=Fabric API Version: +creator.ui.use_fabricapi.label=Use Fabric API +creator.ui.spongeapi_version.label=Sponge Version: +creator.ui.velocity_version.label=Velocity Version: +creator.ui.versions_download.label=Downloading versions... + +creator.ui.warn.no_yarn_to_mc_match=Unable to match Yarn versions to Minecraft version +creator.ui.warn.no_fabricapi_to_mc_match=Unable to match API versions to Minecraft version creator.ui.outdated.message=Is the Minecraft project wizard outdated? \ Create an issue on the MinecraftDev issue tracker. @@ -61,6 +114,9 @@ creator.ui.generic_unfinished.message=Haven''t finished {0} creator.ui.create_minecraft_project=Create a new Minecraft project creator.step.generic.project_created.message=Your project is being created +creator.step.generic.init_template_providers.message=Initializing templates +creator.step.generic.load_template.message=Loading templates +creator.step.generic.no_templates_available.message=There are no templates available creator.step.gradle.patch_gradle.description=Patching Gradle files creator.step.gradle.import_gradle.description=Importing Gradle project @@ -72,8 +128,15 @@ creator.step.maven.import_maven.description=Importing Maven project creator.step.reformat.description=Reformatting files +creator.validation.custom.path_not_a_directory=Path is not a directory +creator.validation.custom.path_not_a_file=Path is not a file + +creator.validation.blank=Must not be blank creator.validation.group_id_non_example=Group ID must be changed from "org.example" creator.validation.semantic_version=Version must be a valid semantic version +creator.validation.class_fqn=Must be a valid class fully qualified name +creator.validation.regex=Must match regex {0} +creator.validation.invalid_option=Selection is not a valid option creator.validation.jdk_preferred=Java {0} is recommended for {1} creator.validation.jdk_preferred_default_reason=these settings @@ -207,6 +270,14 @@ minecraft.settings.show_chat_color_underlines=Show chat color underlines minecraft.settings.chat_color_underline_style=Chat color underline style: minecraft.settings.mixin=Mixin minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line +minecraft.settings.creator=Creator +minecraft.settings.creator.repos=Template Repositories: +minecraft.settings.creator.repos.column.name=Name +minecraft.settings.creator.repos.column.provider=Provider +minecraft.settings.creator.repo_config.title={0} Template Repo Configuration +minecraft.settings.creator.repo.default_name=My Repo +minecraft.settings.creator.repo.builtin_name=Built In + minecraft.settings.lang_template.display_name=Localization Template minecraft.settings.lang_template.scheme=Scheme: minecraft.settings.lang_template.project_must_be_selected=You must have selected a project for this! @@ -216,3 +287,8 @@ minecraft.settings.lang_template.comment=You may edit the template used fo minecraft.settings.translation=Translation minecraft.settings.translation.force_json_translation_file=Force JSON translation file (1.13+) minecraft.settings.translation.use_custom_convert_template=Use custom template for convert literal to translation + +template.provider.builtin.label=Built In +template.provider.remote.label=Remote +template.provider.local.label=Local +template.provider.zip.label=Archive diff --git a/src/main/resources/messages/MinecraftDevelopment_zh.properties b/src/main/resources/messages/MinecraftDevelopment_zh.properties index 5cac19f5d..c1f1e223e 100644 --- a/src/main/resources/messages/MinecraftDevelopment_zh.properties +++ b/src/main/resources/messages/MinecraftDevelopment_zh.properties @@ -18,7 +18,7 @@ # along with this program. If not, see . # -creator.ui.build_system.label.generic=构建系统: +creator.ui.build_system.label=构建系统: creator.ui.build_system.label.gradle=Gradle creator.ui.build_system.label.maven=Maven @@ -45,7 +45,7 @@ creator.ui.issue_tracker.label=Issue Tracker: creator.ui.update_url.label=更新 URL: creator.ui.depend.label=依赖: creator.ui.soft_depend.label=软依赖: -creator.ui.mixins.label=使用 Mixins: +creator.ui.use_mixins.label=使用 Mixins: creator.ui.parchment.label=Parchment: creator.ui.parchment.include.label=Include: creator.ui.parchment.include.old_mc.label=更旧的 Minecraft 版本 diff --git a/templates b/templates new file mode 160000 index 000000000..c8cf7b83d --- /dev/null +++ b/templates @@ -0,0 +1 @@ +Subproject commit c8cf7b83d9f15903c40e603725318de5bcba85f8 From 6b0471e93679cb6cd2b0d1d9c39409c1cbf811ca Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Fri, 12 Jul 2024 21:16:19 +0100 Subject: [PATCH 26/37] Add support for MixinExtras expressions (#2274) * Start on MixinExtras Expression language * MEExpression color settings page * MEExpression annotator * MEExpression brace matcher and quote handler * Switch LHS of MEExpression assignmentExpression to themselves be certain types of expression * MEExpression language injection inside @Expression * Fix formatting and licenses * Add MIXINEXTRAS:EXPRESSION injection point and add @Expression annotation on completion * Fix licenser errors * Add new ME expression features * Implement MixinExtras expression collect visitor * Fix cast expressions * Simple best-effort source matching for ME expressions * Fix name expression source matching * Fix MEName.isWildcard * Fix MELitExpression source matching * operationSign - operationTokenType * Add built-in definitions * Update MixinExtras * Start with ME definition references * Attempt to overhaul ME expression injection * Some fixes to the new injection + navigation * MixinExtras: Add handler signature support for expressions. (#2244) * Partially fix ME definition renaming * Attempt to get inplace rename refactoring to work (it doesn't) * MixinExtras: Use expression-suggested parameter names if they're present. (#2257) * Fix MEExpressionInjector. Rename refactoring works! * Suppress deprecation warning * ME expression `@Definition` find usages * Fix/expressions int like types (#2261) * Mixin: Combine parameter and return type inspections. * MixinExtras: Offer a choice between all valid int-like types. * Mixin: Fix tests for handler signature inspection. * Add simple keyword completion to ME expressions * Why didn't my local ktlint tell me about these * Store whether a declaration is a type in the ME PSI * Add completions for items that already have a definition * Extract some ME expression matching into its own class, and cache some more things * Remove some debug code * First attempt at bytecode-based completion (it's broken) * Bytecode-based completion fixes * Add new definition annotations below existing definition annotations, or at the top * Fix cursor offset * Add utilities to textify various ASM nodes * Add expression variants, to allow MixinExtras to match expressions as if they were other types of expressions * Merge two expression types into a single MENewExpression, improve ME expression completion * Add better completion tail types * Fix completion not working inside of constructors * Add errors/warnings for unused and unresolved definitions, and highlight primitive types specially * Split MatchUtil into CompletionUtil * Fold @At.target for definition completions * Local variable completions * Some fixes to local variable completion * Fix can-be-implicit locals with inaccessible types not showing template * Show field and method types in completion list * Add completion tests, and make some fixes to completion * Fix folding on completion * Refactor mcdev settings, move shadow setting into a project setting * Add setting for position of @Definition relative to @Expression * Add folding for @Definitions * Fix array literal input completion * Fix ktlint * Relax uniqueness in ME expression completions * Initial switch from @Definition.at to field and method * Fix tests * Add references to @Definition.field and method * Add folding to @Definition.field and method * Add comments to MEExpressionCompletionUtil to explain the completion process in more detail * Fix @Local.type extraction * Add string literal completion for ME expressions * Fix @Local.type for source matching too * Fix parsing of multiple @Expression annotations in a single modifier list * Format MEExpressionLexer.flex * Better handle parenthesized expressions in Java source matching * Add method reference expressions * Fix presentable name of method reference completions * Override matches(Handle) for method MemberDefinition * Update MixinExtras and don't assume that instructions are part of the method * Wrap expanded instructions in a custom type to prevent them accidentally being used when an instruction in the original method is expected or vice versa. * Fix IdentityHashMap with VirtualInsn keys * Update MixinExtras * Fix completion of new expressions with new MixinExtras update * Update MixinExtras, fixes compound instructions * New: Add support for string concat expressions in MixinExtras. (#2281) * Address PR comments * MixinExtras Expressions: Migrate to Expressions library. * Fix: Resolve being unable to get the descriptor for some complex types. * MixinExtras Expressions: Recreate `ClassInfo#getCommonSuperClassOrInterface`. It's quite scuffed and often returns `Object` even when there is a suitable common interface, but what's important is that this matches the runtime logic. * MixinExtras: Fix completion confidence. * Expressions: Autocomplete `method`s and `field`s using flows not instructions. * Expressions: Autocomplete `method`s for method references. * Expressions: A class constant is an expression. * Expressions: Show instantiation desc in autocomplete. * Expressions: Make completions always unique to stop them being filtered. We filter duplicates ourselves. * Expressions: Overhaul array completions. Make the preview text more representative of the actual completion and support array literals which were previously missing. * Expressions: Fix super call completions. * Expressions: Confidently suggest completions after `::`. * Expressions: Add intention action to define unresolved identifiers. * Expressions: Fix `@Local`s not properly handling compound insns. Also adapt to related MixinExtras changes. * Refactor: Add `project` as parameter to `MEExpressionCompletionUtil.addDefinition` * Use maven central MixinExtras library * Remove reference to light service in plugin.xml * Use ReentrantReadWriteLock.read and write extension functions --------- Co-authored-by: LlamaLad7 --- build.gradle.kts | 11 + gradle/libs.versions.toml | 3 + .../meExpression/MEExpressionTestData.java | 84 ++ src/main/grammars/MEExpressionLexer.flex | 146 ++ src/main/grammars/MEExpressionParser.bnf | 335 +++++ src/main/kotlin/MinecraftConfigurable.kt | 7 - .../kotlin/MinecraftProjectConfigurable.kt | 64 + src/main/kotlin/MinecraftProjectSettings.kt | 46 + src/main/kotlin/MinecraftSettings.kt | 8 - src/main/kotlin/asset/MCDevBundle.kt | 6 + .../nbt/lang/colors/NbttColorSettingsPage.kt | 27 +- .../mixin/action/GenerateShadowAction.kt | 4 +- .../completion/MixinCompletionConfidence.kt | 1 + .../expression/MEDefinitionFoldingBuilder.kt | 117 ++ .../mixin/expression/MEExpressionAnnotator.kt | 368 +++++ .../expression/MEExpressionBraceMatcher.kt | 41 + .../MEExpressionColorSettingsPage.kt | 153 ++ .../MEExpressionCompletionContributor.kt | 138 ++ .../expression/MEExpressionCompletionUtil.kt | 1339 +++++++++++++++++ .../expression/MEExpressionElementFactory.kt | 73 + .../mixin/expression/MEExpressionFileType.kt | 31 + .../mixin/expression/MEExpressionInjector.kt | 207 +++ .../mixin/expression/MEExpressionLanguage.kt | 25 + .../expression/MEExpressionLexerAdapter.kt | 25 + .../mixin/expression/MEExpressionMatchUtil.kt | 317 ++++ .../MEExpressionParserDefinition.kt | 46 + .../expression/MEExpressionQuoteHandler.kt | 26 + .../MEExpressionRefactoringSupport.kt | 29 + .../mixin/expression/MEExpressionService.kt | 76 + .../MEExpressionSyntaxHighlighter.kt | 198 +++ .../MEExpressionTypedHandlerDelegate.kt | 42 + .../mixin/expression/MEFlowContext.kt | 26 + .../mixin/expression/MESourceMatchContext.kt | 98 ++ .../expression/psi/MEExpressionElementType.kt | 27 + .../mixin/expression/psi/MEExpressionFile.kt | 41 + .../expression/psi/MEExpressionParserUtil.kt | 45 + .../expression/psi/MEExpressionTokenSets.kt | 73 + .../expression/psi/MEExpressionTokenType.kt | 29 + .../expression/psi/MEMatchableElement.kt | 31 + .../psi/MENameElementManipulator.kt | 34 + .../mixin/expression/psi/MEPsiUtil.kt | 58 + .../psi/MERecursiveWalkingVisitor.kt | 45 + .../mixin/expression/psi/METypeUtil.kt | 125 ++ .../expression/psi/mixins/MEArgumentsMixin.kt | 34 + .../mixins/MEArrayAccessExpressionMixin.kt | 29 + .../psi/mixins/MEBinaryExpressionMixin.kt | 30 + .../psi/mixins/MECastExpressionMixin.kt | 30 + .../psi/mixins/MEDeclarationItemMixin.kt | 27 + .../psi/mixins/MELitExpressionMixin.kt | 31 + .../expression/psi/mixins/MENameMixin.kt | 28 + .../psi/mixins/MENewExpressionMixin.kt | 35 + .../expression/psi/mixins/METypeMixin.kt | 32 + .../psi/mixins/MEUnaryExpressionMixin.kt | 28 + .../psi/mixins/impl/MEArgumentsImplMixin.kt | 44 + .../impl/MEArrayAccessExpressionImplMixin.kt | 58 + .../mixins/impl/MEAssignStatementImplMixin.kt | 57 + .../impl/MEBinaryExpressionImplMixin.kt | 124 ++ .../MEBoundReferenceExpressionImplMixin.kt | 64 + .../impl/MECapturingExpressionImplMixin.kt | 38 + .../mixins/impl/MECastExpressionImplMixin.kt | 66 + .../MEClassConstantExpressionImplMixin.kt | 60 + ...ConstructorReferenceExpressionImplMixin.kt | 47 + .../psi/mixins/impl/MEDeclarationImplMixin.kt | 63 + .../mixins/impl/MEDeclarationItemImplMixin.kt | 32 + .../psi/mixins/impl/MEExpressionImplMixin.kt | 38 + .../impl/MEExpressionStatementImplMixin.kt | 37 + ...EFreeMethodReferenceExpressionImplMixin.kt | 60 + .../mixins/impl/MELitExpressionImplMixin.kt | 124 ++ .../impl/MEMemberAccessExpressionImplMixin.kt | 71 + .../impl/MEMethodCallExpressionImplMixin.kt | 77 + .../mixins/impl/MENameExpressionImplMixin.kt | 78 + .../psi/mixins/impl/MENameImplMixin.kt | 41 + .../mixins/impl/MENewExpressionImplMixin.kt | 138 ++ .../MEParenthesizedExpressionImplMixin.kt | 37 + .../mixins/impl/MEReturnStatementImplMixin.kt | 43 + .../psi/mixins/impl/MEStatementImplMixin.kt | 38 + .../MEStaticMethodCallExpressionImplMixin.kt | 60 + .../impl/MESuperCallExpressionImplMixin.kt | 60 + .../mixins/impl/METhisExpressionImplMixin.kt | 36 + .../mixins/impl/METhrowStatementImplMixin.kt | 44 + .../psi/mixins/impl/METypeImplMixin.kt | 53 + .../mixins/impl/MEUnaryExpressionImplMixin.kt | 65 + .../reference/MEDefinitionReference.kt | 70 + .../MEExpressionFindUsagesProvider.kt | 38 + .../folding/MixinFoldingOptionsProvider.kt | 10 + .../mixin/folding/MixinFoldingSettings.kt | 2 + .../mixin/handlers/InjectAnnotationHandler.kt | 3 + .../handlers/InjectorAnnotationHandler.kt | 5 + .../mixin/handlers/ModifyArgHandler.kt | 3 + .../mixin/handlers/ModifyArgsHandler.kt | 3 + .../mixin/handlers/ModifyConstantHandler.kt | 3 + .../mixin/handlers/ModifyVariableHandler.kt | 3 + .../mixin/handlers/RedirectInjectorHandler.kt | 3 + .../handlers/injectionPoint/AtResolver.kt | 1 + .../handlers/injectionPoint/InjectionPoint.kt | 14 +- .../injectionPoint/LoadInjectionPoint.kt | 45 +- .../mixinextras/ExpressionInjectionPoint.kt | 284 ++++ .../MixinExtrasInjectorAnnotationHandler.kt | 136 +- .../ModifyExpressionValueHandler.kt | 40 +- .../mixinextras/ModifyReceiverHandler.kt | 7 +- .../mixinextras/ModifyReturnValueHandler.kt | 8 +- .../mixin/handlers/mixinextras/TargetInsn.kt | 30 + .../handlers/mixinextras/WrapMethodHandler.kt | 3 + .../mixinextras/WrapOperationHandler.kt | 54 +- .../mixinextras/WrapWithConditionHandler.kt | 7 +- ...nvalidInjectorMethodSignatureInspection.kt | 222 ++- .../inspection/injector/MethodSignature.kt | 20 +- .../MixinClassCastInspectionSuppressor.kt | 4 +- .../reference/MixinReferenceContributor.kt | 12 + .../mixin/reference/MixinSelectors.kt | 67 +- .../target/DefinitionReferenceGTDHandler.kt | 45 + .../reference/target/DefinitionReferences.kt | 182 +++ .../kotlin/platform/mixin/util/AsmDfaUtil.kt | 4 +- .../kotlin/platform/mixin/util/AsmUtil.kt | 197 ++- .../kotlin/platform/mixin/util/LocalInfo.kt | 23 + .../platform/mixin/util/LocalVariables.kt | 20 +- .../platform/mixin/util/MixinConstants.kt | 3 + .../mixin/util/UnsafeCachedValueCapture.kt | 28 + src/main/kotlin/util/BeforeOrAfter.kt | 32 + src/main/kotlin/util/MemberReference.kt | 91 +- src/main/kotlin/util/bytecode-utils.kt | 4 +- src/main/kotlin/util/psi-utils.kt | 58 + src/main/resources/META-INF/plugin.xml | 36 + .../messages/MinecraftDevelopment.properties | 58 +- .../kotlin/platform/mixin/BaseMixinTest.kt | 5 +- .../InvalidInjectorMethodSignatureFixTest.kt | 2 +- ...idInjectorMethodSignatureInspectionTest.kt | 6 +- .../expression/MEExpressionCompletionTest.kt | 645 ++++++++ 128 files changed, 8901 insertions(+), 317 deletions(-) create mode 100644 mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java create mode 100644 src/main/grammars/MEExpressionLexer.flex create mode 100644 src/main/grammars/MEExpressionParser.bnf create mode 100644 src/main/kotlin/MinecraftProjectConfigurable.kt create mode 100644 src/main/kotlin/MinecraftProjectSettings.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionService.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MEFlowContext.kt create mode 100644 src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt create mode 100644 src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt create mode 100644 src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt create mode 100644 src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt create mode 100644 src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt create mode 100644 src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt create mode 100644 src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt create mode 100644 src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt create mode 100644 src/main/kotlin/util/BeforeOrAfter.kt create mode 100644 src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 48e4ab67b..450c44ad1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -107,12 +107,17 @@ repositories { } } mavenCentral() + maven("https://repo.spongepowered.org/maven/") } dependencies { // Add tools.jar for the JDI API implementation(files(Jvm.current().toolsJar)) + implementation(libs.mixinExtras.expressions) + testLibs(libs.mixinExtras.common) + implementation("org.ow2.asm:asm-util:9.3") + // Kotlin implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) @@ -198,6 +203,7 @@ intellij { "Kotlin", "org.toml.lang:$pluginTomlVersion", "ByteCodeViewer", + "org.intellij.intelliLang", "properties", // needed dependencies for unit tests "junit" @@ -363,6 +369,9 @@ val generateNbttParser by parser("NbttParser", "com/demonwav/mcdev/nbt/lang/gen" val generateLangLexer by lexer("LangLexer", "com/demonwav/mcdev/translations/lang/gen") val generateLangParser by parser("LangParser", "com/demonwav/mcdev/translations/lang/gen") +val generateMEExpressionLexer by lexer("MEExpressionLexer", "com/demonwav/mcdev/platform/mixin/expression/gen") +val generateMEExpressionParser by parser("MEExpressionParser", "com/demonwav/mcdev/platform/mixin/expression/gen") + val generateTranslationTemplateLexer by lexer( "TranslationTemplateLexer", "com/demonwav/mcdev/translations/template/gen" @@ -381,6 +390,8 @@ val generate by tasks.registering { generateNbttParser, generateLangLexer, generateLangParser, + generateMEExpressionLexer, + generateMEExpressionParser, generateTranslationTemplateLexer, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f8dabc6d7..941db8f7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", ve coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } mappingIo = "net.fabricmc:mapping-io:0.2.1" +mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.1" # GrammarKit jflex-lib = "org.jetbrains.idea:jflex:1.7.0-b7f882a" @@ -40,6 +41,8 @@ junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "jun junit-entine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform" } +mixinExtras-common = "io.github.llamalad7:mixinextras-common:0.5.0-beta.1" + [bundles] coroutines = ["coroutines-core", "coroutines-jdk8", "coroutines-swing"] asm = ["asm", "asm-tree", "asm-analysis"] diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java new file mode 100644 index 000000000..c5efbe9a6 --- /dev/null +++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java @@ -0,0 +1,84 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.mixintestdata.meExpression; + +import java.util.ArrayList; +import java.util.stream.Stream; + +public class MEExpressionTestData { + private static final SynchedData STINGER_COUNT = null; + private SynchedDataManager synchedData; + + public void complexFunction() { + int one = 1; + String local1 = "Hello"; + String local2 = "World"; + + System.out.println(new StringBuilder(local1).append(", ").append(local2)); + System.out.println(one); + + new ArrayList<>(10); + + InaccessibleType varOfInaccessibleType = new InaccessibleType(); + acceptInaccessibleType(varOfInaccessibleType); + noArgMethod(); + + String[] strings1 = new String[] { local1, local2 }; + String[] strings2 = new String[one]; + + Stream.empty().map(this::nonStaticMapper).map(MEExpressionTestData::staticMapper).map(ConstructedByMethodReference::new); + } + + private static void acceptInaccessibleType(InaccessibleType type) { + } + + private static void noArgMethod() { + } + + public int getStingerCount() { + return (Integer) this.synchedData.get(STINGER_COUNT); + } + + private Object nonStaticMapper(Object arg) { + return arg; + } + + private static Object staticMapper(Object arg) { + return arg; + } + + private static class InaccessibleType { + + } + + public static class SynchedDataManager { + public V get(SynchedData data) { + return null; + } + } + + public static class SynchedData { + } + + public static class ConstructedByMethodReference { + public ConstructedByMethodReference(Object bar) {} + } +} diff --git a/src/main/grammars/MEExpressionLexer.flex b/src/main/grammars/MEExpressionLexer.flex new file mode 100644 index 000000000..c7d9fad8a --- /dev/null +++ b/src/main/grammars/MEExpressionLexer.flex @@ -0,0 +1,146 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression; + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes; +import com.intellij.lexer.FlexLexer; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.TokenType; + +%% + +%public +%class MEExpressionLexer +%implements FlexLexer +%function advance +%type IElementType + +%state STRING + +%unicode + +WHITE_SPACE = [\ \n\t\r] +RESERVED = assert|break|case|catch|const|continue|default|else|finally|for|goto|if|switch|synchronized|try|while|yield|_ +WILDCARD = "?" +NEW = new +INSTANCEOF = instanceof +BOOL_LIT = true|false +NULL_LIT = null +DO = do +RETURN = return +THROW = throw +THIS = this +SUPER = super +CLASS = class +IDENTIFIER = [A-Za-z_][A-Za-z0-9_]* +INT_LIT = ( [0-9]+ | 0x[0-9a-fA-F]+ ) +DEC_LIT = [0-9]*\.[0-9]+ +PLUS = "+" +MINUS = - +MULT = "*" +DIV = "/" +MOD = % +BITWISE_NOT = "~" +DOT = "." +COMMA = , +LEFT_PAREN = "(" +RIGHT_PAREN = ")" +LEFT_BRACKET = "[" +RIGHT_BRACKET = "]" +LEFT_BRACE = "{" +RIGHT_BRACE = "}" +AT = @ +SHL = << +SHR = >> +USHR = >>> +LT = < +LE = <= +GT = > +GE = >= +EQ = == +NE = "!=" +BITWISE_AND = & +BITWISE_XOR = "^" +BITWISE_OR = "|" +ASSIGN = = +METHOD_REF = :: + +STRING_TERMINATOR = ' +STRING_ESCAPE = \\'|\\\\ + +%% + + { + {WHITE_SPACE}+ { return TokenType.WHITE_SPACE; } + {RESERVED} { return MEExpressionTypes.TOKEN_RESERVED; } + {WILDCARD} { return MEExpressionTypes.TOKEN_WILDCARD; } + {NEW} { return MEExpressionTypes.TOKEN_NEW; } + {INSTANCEOF} { return MEExpressionTypes.TOKEN_INSTANCEOF; } + {BOOL_LIT} { return MEExpressionTypes.TOKEN_BOOL_LIT; } + {NULL_LIT} { return MEExpressionTypes.TOKEN_NULL_LIT; } + {DO} { return MEExpressionTypes.TOKEN_DO; } + {RETURN} { return MEExpressionTypes.TOKEN_RETURN; } + {THROW} { return MEExpressionTypes.TOKEN_THROW; } + {THIS} { return MEExpressionTypes.TOKEN_THIS; } + {SUPER} { return MEExpressionTypes.TOKEN_SUPER; } + {CLASS} { return MEExpressionTypes.TOKEN_CLASS; } + {IDENTIFIER} { return MEExpressionTypes.TOKEN_IDENTIFIER; } + {INT_LIT} { return MEExpressionTypes.TOKEN_INT_LIT; } + {DEC_LIT} { return MEExpressionTypes.TOKEN_DEC_LIT; } + {PLUS} { return MEExpressionTypes.TOKEN_PLUS; } + {MINUS} { return MEExpressionTypes.TOKEN_MINUS; } + {MULT} { return MEExpressionTypes.TOKEN_MULT; } + {DIV} { return MEExpressionTypes.TOKEN_DIV; } + {MOD} { return MEExpressionTypes.TOKEN_MOD; } + {BITWISE_NOT} { return MEExpressionTypes.TOKEN_BITWISE_NOT; } + {DOT} { return MEExpressionTypes.TOKEN_DOT; } + {COMMA} { return MEExpressionTypes.TOKEN_COMMA; } + {LEFT_PAREN} { return MEExpressionTypes.TOKEN_LEFT_PAREN; } + {RIGHT_PAREN} { return MEExpressionTypes.TOKEN_RIGHT_PAREN; } + {LEFT_BRACKET} { return MEExpressionTypes.TOKEN_LEFT_BRACKET; } + {RIGHT_BRACKET} { return MEExpressionTypes.TOKEN_RIGHT_BRACKET; } + {LEFT_BRACE} { return MEExpressionTypes.TOKEN_LEFT_BRACE; } + {RIGHT_BRACE} { return MEExpressionTypes.TOKEN_RIGHT_BRACE; } + {AT} { return MEExpressionTypes.TOKEN_AT; } + {SHL} { return MEExpressionTypes.TOKEN_SHL; } + {SHR} { return MEExpressionTypes.TOKEN_SHR; } + {USHR} { return MEExpressionTypes.TOKEN_USHR; } + {LT} { return MEExpressionTypes.TOKEN_LT; } + {LE} { return MEExpressionTypes.TOKEN_LE; } + {GT} { return MEExpressionTypes.TOKEN_GT; } + {GE} { return MEExpressionTypes.TOKEN_GE; } + {EQ} { return MEExpressionTypes.TOKEN_EQ; } + {NE} { return MEExpressionTypes.TOKEN_NE; } + {BITWISE_AND} { return MEExpressionTypes.TOKEN_BITWISE_AND; } + {BITWISE_XOR} { return MEExpressionTypes.TOKEN_BITWISE_XOR; } + {BITWISE_OR} { return MEExpressionTypes.TOKEN_BITWISE_OR; } + {ASSIGN} { return MEExpressionTypes.TOKEN_ASSIGN; } + {METHOD_REF} { return MEExpressionTypes.TOKEN_METHOD_REF; } + {STRING_TERMINATOR} { yybegin(STRING); return MEExpressionTypes.TOKEN_STRING_TERMINATOR; } +} + + { + {STRING_ESCAPE} { return MEExpressionTypes.TOKEN_STRING_ESCAPE; } + {STRING_TERMINATOR} { yybegin(YYINITIAL); return MEExpressionTypes.TOKEN_STRING_TERMINATOR; } + [^'\\]+ { return MEExpressionTypes.TOKEN_STRING; } +} + +[^] { return TokenType.BAD_CHARACTER; } diff --git a/src/main/grammars/MEExpressionParser.bnf b/src/main/grammars/MEExpressionParser.bnf new file mode 100644 index 000000000..47d509e01 --- /dev/null +++ b/src/main/grammars/MEExpressionParser.bnf @@ -0,0 +1,335 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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 . + */ + +{ + parserClass="com.demonwav.mcdev.platform.mixin.expression.gen.MEExpressionParser" + extends="com.intellij.extapi.psi.ASTWrapperPsiElement" + parserImports = ["static com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionParserUtil.*"] + + psiClassPrefix="ME" + psiImplClassSuffix="Impl" + psiPackage="com.demonwav.mcdev.platform.mixin.expression.gen.psi" + psiImplPackage="com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl" + + elementTypeHolderClass="com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes" + elementTypeClass="com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionElementType" + tokenTypeClass="com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenType" + + tokens = [ + TOKEN_RESERVED = "TOKEN_RESERVED" + ] + + extends(".+Expression") = expression + extends(".+Statement") = statement +} + +meExpressionFile ::= item* <> + +item ::= declarationItem | statementItem + +declarationItem ::= TOKEN_CLASS TOKEN_BOOL_LIT declaration { + pin = 1 + extends = item + implements = [ + "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEDeclarationItemMixin" + ] + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEDeclarationItemImplMixin" +} + +declaration ::= TOKEN_IDENTIFIER { + implements = [ + "com.intellij.psi.PsiNamedElement" + "com.intellij.psi.PsiNameIdentifierOwner" + "com.intellij.psi.NavigatablePsiElement" + ] + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEDeclarationImplMixin" +} + +statementItem ::= TOKEN_DO TOKEN_LEFT_BRACE statement TOKEN_RIGHT_BRACE { + pin = 1 + extends = item +} + +private statementRecover ::= !TOKEN_RIGHT_BRACE + +statement ::= assignStatement | + returnStatement | + throwStatement | + expressionStatement { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEStatementImplMixin" + recoverWhile = statementRecover +} + +assignStatement ::= assignableExpression TOKEN_ASSIGN expression { + pin = 2 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEAssignStatementImplMixin" + methods = [ + targetExpr = "expression[0]" + rightExpr = "expression[1]" + ] +} + +private assignableExpression ::= arrayAccessExpression | memberAccessExpression | nameExpression + +returnStatement ::= TOKEN_RETURN expression { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEReturnStatementImplMixin" + methods = [ + valueExpr = "expression" + ] +} + +throwStatement ::= TOKEN_THROW expression { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METhrowStatementImplMixin" + methods = [ + valueExpr = "expression" + ] +} + +expressionStatement ::= expression { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEExpressionStatementImplMixin" +} + +private exprRecover ::= !( TOKEN_COMMA | TOKEN_RIGHT_PAREN | TOKEN_RIGHT_BRACKET | TOKEN_RIGHT_BRACE ) + +expression ::= capturingExpression | + superCallExpression | + staticMethodCallExpression | + classConstantExpression | + unaryExpression | + binaryExpression | + castExpression | + parenthesizedExpression | + methodCallExpression | + boundMethodReferenceExpression | + freeMethodReferenceExpression | + constructorReferenceExpression | + arrayAccessExpression | + memberAccessExpression | + newExpression | + litExpression | + thisExpression | + nameExpression { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEExpressionImplMixin" + recoverWhile = exprRecover +} + +external rightParen ::= parseToRightBracket exprRecover TOKEN_RIGHT_PAREN +external rightBracket ::= parseToRightBracket exprRecover TOKEN_RIGHT_BRACKET +external rightBrace ::= parseToRightBracket exprRecover TOKEN_RIGHT_BRACE + +capturingExpression ::= TOKEN_AT TOKEN_LEFT_PAREN expression rightParen { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MECapturingExpressionImplMixin" +} + +parenthesizedExpression ::= TOKEN_LEFT_PAREN expression rightParen { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEParenthesizedExpressionImplMixin" +} + +superCallExpression ::= TOKEN_SUPER TOKEN_DOT name TOKEN_LEFT_PAREN arguments rightParen { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MESuperCallExpressionImplMixin" + methods = [ + memberName = "name" + ] +} + +methodCallExpression ::= expression TOKEN_DOT name TOKEN_LEFT_PAREN arguments rightParen { + pin = 4 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEMethodCallExpressionImplMixin" + methods = [ + receiverExpr = "expression" + memberName = "name" + ] +} + +staticMethodCallExpression ::= name TOKEN_LEFT_PAREN arguments rightParen { + pin = 2 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEStaticMethodCallExpressionImplMixin" + methods = [ + memberName = "name" + ] +} + +boundMethodReferenceExpression ::= expression !(TOKEN_METHOD_REF TOKEN_NEW) TOKEN_METHOD_REF name { + pin = 3 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEBoundReferenceExpressionImplMixin" + methods = [ + receiverExpr = "expression" + memberName = "name" + ] +} + +freeMethodReferenceExpression ::= TOKEN_METHOD_REF name { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEFreeMethodReferenceExpressionImplMixin" + methods = [ + memberName = "name" + ] +} + +constructorReferenceExpression ::= type TOKEN_METHOD_REF TOKEN_NEW { + pin = 3 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEConstructorReferenceExpressionImplMixin" + methods = [ + className = "type" + ] +} + +arrayAccessExpression ::= expression TOKEN_LEFT_BRACKET expression? rightBracket { + pin = 2 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArrayAccessExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEArrayAccessExpressionImplMixin" + methods = [ + arrayExpr = "expression[0]" + indexExpr = "expression[1]" + ] +} + +classConstantExpression ::= type TOKEN_DOT TOKEN_CLASS { + pin = 3 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEClassConstantExpressionImplMixin" + methods = [ + className = "name" + ] +} + +memberAccessExpression ::= expression TOKEN_DOT name { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEMemberAccessExpressionImplMixin" + methods = [ + receiverExpr = "expression" + memberName = "name" + ] +} + +unaryExpression ::= ((TOKEN_MINUS !(TOKEN_DEC_LIT | TOKEN_INT_LIT)) | TOKEN_BITWISE_NOT) expression { + pin = 2 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEUnaryExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEUnaryExpressionImplMixin" +} + +castExpression ::= parenthesizedExpression expression { + rightAssociative = true + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MECastExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MECastExpressionImplMixin" +} + +binaryExpression ::= expression binaryOp expression { + pin = 2 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEBinaryExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEBinaryExpressionImplMixin" + methods = [ + leftExpr = "expression[0]" + rightExpr = "expression[1]" + ] +} + +private binaryOp ::= multiplicativeOp | + additiveOp | + shiftOp | + comparisonOp | + TOKEN_INSTANCEOF | + equalityOp | + TOKEN_BITWISE_AND | + TOKEN_BITWISE_XOR | + TOKEN_BITWISE_OR + +private multiplicativeOp ::= TOKEN_MULT | TOKEN_DIV | TOKEN_MOD +private additiveOp ::= TOKEN_PLUS | TOKEN_MINUS +private shiftOp ::= TOKEN_SHL | TOKEN_SHR | TOKEN_USHR +private comparisonOp ::= TOKEN_LT | TOKEN_LE | TOKEN_GT | TOKEN_GE +private equalityOp ::= TOKEN_EQ | TOKEN_NE + +newExpression ::= TOKEN_NEW name ( + (TOKEN_LEFT_PAREN arguments rightParen) | + ( + TOKEN_LEFT_BRACKET expression? rightBracket + ( TOKEN_LEFT_BRACKET expression? rightBracket )* + ( TOKEN_LEFT_BRACE arguments rightBrace )? + ) +) { + pin = 1 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENewExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENewExpressionImplMixin" + methods = [ + type = "name" + dimExprs = "expression" + ] +} + +litExpression ::= decimalLitExpression | intLitExpression | stringLitExpression | boolLitExpression | nulLLitExpression { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MELitExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MELitExpressionImplMixin" +} + +private decimalLitExpression ::= TOKEN_MINUS? TOKEN_DEC_LIT { + extends = litExpression +} + +private intLitExpression ::= TOKEN_MINUS? TOKEN_INT_LIT { + extends = litExpression +} + +private stringLitExpression ::= TOKEN_STRING_TERMINATOR ( TOKEN_STRING | TOKEN_STRING_ESCAPE )* TOKEN_STRING_TERMINATOR { + pin = 1 + extends = litExpression +} + +private boolLitExpression ::= TOKEN_BOOL_LIT { + extends = litExpression +} + +private nulLLitExpression ::= TOKEN_NULL_LIT { + extends = litExpression +} + +thisExpression ::= TOKEN_THIS { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METhisExpressionImplMixin" +} + +nameExpression ::= name { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENameExpressionImplMixin" + methods = [ + MEName = "name" + ] +} + +type ::= name ( TOKEN_LEFT_BRACKET TOKEN_RIGHT_BRACKET )* { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.METypeMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METypeImplMixin" + methods = [ + MEName = "name" + ] +} + +name ::= TOKEN_IDENTIFIER | TOKEN_WILDCARD { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENameMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENameImplMixin" +} + +arguments ::= (expression (TOKEN_COMMA expression)*)? { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArgumentsMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEArgumentsImplMixin" +} diff --git a/src/main/kotlin/MinecraftConfigurable.kt b/src/main/kotlin/MinecraftConfigurable.kt index 60f5a32ab..12fc11567 100644 --- a/src/main/kotlin/MinecraftConfigurable.kt +++ b/src/main/kotlin/MinecraftConfigurable.kt @@ -86,13 +86,6 @@ class MinecraftConfigurable : Configurable { } } - group(MCDevBundle("minecraft.settings.mixin")) { - row { - checkBox(MCDevBundle("minecraft.settings.mixin.shadow_annotation_same_line")) - .bindSelected(settings::isShadowAnnotationsSameLine) - } - } - group(MCDevBundle("minecraft.settings.creator")) { row(MCDevBundle("minecraft.settings.creator.repos")) {} diff --git a/src/main/kotlin/MinecraftProjectConfigurable.kt b/src/main/kotlin/MinecraftProjectConfigurable.kt new file mode 100644 index 000000000..0e676f0b7 --- /dev/null +++ b/src/main/kotlin/MinecraftProjectConfigurable.kt @@ -0,0 +1,64 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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 + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.util.BeforeOrAfter +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogPanel +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent +import org.jetbrains.annotations.Nls + +class MinecraftProjectConfigurable(private val project: Project) : Configurable { + private lateinit var panel: DialogPanel + + @Nls + override fun getDisplayName() = MCDevBundle("minecraft.settings.project.display_name") + + override fun createComponent(): JComponent = panel { + val settings = MinecraftProjectSettings.getInstance(project) + + group(MCDevBundle("minecraft.settings.mixin")) { + row { + checkBox(MCDevBundle("minecraft.settings.mixin.shadow_annotation_same_line")) + .bindSelected(settings::isShadowAnnotationsSameLine) + } + row { + label(MCDevBundle("minecraft.settings.mixin.definition_pos_relative_to_expression")) + comboBox(EnumComboBoxModel(BeforeOrAfter::class.java)) + .bindItem(settings::definitionPosRelativeToExpression) { + settings.definitionPosRelativeToExpression = it ?: BeforeOrAfter.BEFORE + } + } + } + }.also { panel = it } + + override fun isModified(): Boolean = panel.isModified() + + override fun apply() = panel.apply() + + override fun reset() = panel.reset() +} diff --git a/src/main/kotlin/MinecraftProjectSettings.kt b/src/main/kotlin/MinecraftProjectSettings.kt new file mode 100644 index 000000000..f22bf178b --- /dev/null +++ b/src/main/kotlin/MinecraftProjectSettings.kt @@ -0,0 +1,46 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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 + +import com.demonwav.mcdev.util.BeforeOrAfter +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.XmlSerializerUtil + +@Service(Service.Level.PROJECT) +@State(name = "MinecraftSettings", storages = [Storage("minecraft_dev.xml")]) +class MinecraftProjectSettings : PersistentStateComponent { + var isShadowAnnotationsSameLine = true + var definitionPosRelativeToExpression = BeforeOrAfter.BEFORE + + override fun getState() = this + override fun loadState(state: MinecraftProjectSettings) { + XmlSerializerUtil.copyBean(state, this) + } + + companion object { + fun getInstance(project: Project) = project.service() + } +} diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt index 0a924aa64..18ae02acf 100644 --- a/src/main/kotlin/MinecraftSettings.kt +++ b/src/main/kotlin/MinecraftSettings.kt @@ -40,8 +40,6 @@ class MinecraftSettings : PersistentStateComponent { var isShowChatColorUnderlines: Boolean = false, var underlineType: UnderlineType = UnderlineType.DOTTED, - var isShadowAnnotationsSameLine: Boolean = true, - var creatorTemplateRepos: List = listOf(TemplateRepo.makeBuiltinRepo()), ) @@ -108,12 +106,6 @@ class MinecraftSettings : PersistentStateComponent { state.underlineType = underlineType } - var isShadowAnnotationsSameLine: Boolean - get() = state.isShadowAnnotationsSameLine - set(shadowAnnotationsSameLine) { - state.isShadowAnnotationsSameLine = shadowAnnotationsSameLine - } - var creatorTemplateRepos: List get() = state.creatorTemplateRepos.map { it.copy() } set(creatorTemplateRepos) { diff --git a/src/main/kotlin/asset/MCDevBundle.kt b/src/main/kotlin/asset/MCDevBundle.kt index 04b9b3d0b..6246c2bae 100644 --- a/src/main/kotlin/asset/MCDevBundle.kt +++ b/src/main/kotlin/asset/MCDevBundle.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.asset import com.intellij.DynamicBundle +import java.util.function.Supplier import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.PropertyKey @@ -36,4 +37,9 @@ object MCDevBundle : DynamicBundle(BUNDLE) { operator fun invoke(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?): String { return getMessage(key, *params) } + + fun pointer(@PropertyKey(resourceBundle = BUNDLE) key: String) = Supplier { invoke(key) } + + fun pointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?) = + Supplier { invoke(key, params) } } diff --git a/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt b/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt index 712fce2de..dca1c1108 100644 --- a/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt +++ b/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt @@ -88,21 +88,24 @@ class NbttColorSettingsPage : ColorSettingsPage { companion object { private val DESCRIPTORS = arrayOf( - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.keyword.display_name"), KEYWORD), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.string.display_name"), STRING), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.unquoted_string.display_name"), UNQUOTED_STRING), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.name.display_name"), STRING_NAME), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.keyword.display_name"), KEYWORD), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.string.display_name"), STRING), AttributesDescriptor( - MCDevBundle("nbt.lang.highlighting.unquoted_name.display_name"), + MCDevBundle.pointer("nbt.lang.highlighting.unquoted_string.display_name"), + UNQUOTED_STRING + ), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.name.display_name"), STRING_NAME), + AttributesDescriptor( + MCDevBundle.pointer("nbt.lang.highlighting.unquoted_name.display_name"), UNQUOTED_STRING_NAME ), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.byte.display_name"), BYTE), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.short.display_name"), SHORT), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.int.display_name"), INT), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.long.display_name"), LONG), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.float.display_name"), FLOAT), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.double.display_name"), DOUBLE), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.material.display_name"), MATERIAL), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.byte.display_name"), BYTE), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.short.display_name"), SHORT), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.int.display_name"), INT), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.long.display_name"), LONG), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.float.display_name"), FLOAT), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.double.display_name"), DOUBLE), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.material.display_name"), MATERIAL), ) private val map = mapOf( diff --git a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt index f53938550..293543c47 100644 --- a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt +++ b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt @@ -20,7 +20,7 @@ package com.demonwav.mcdev.platform.mixin.action -import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.MinecraftProjectSettings import com.demonwav.mcdev.platform.mixin.util.MixinConstants import com.demonwav.mcdev.platform.mixin.util.findFields import com.demonwav.mcdev.platform.mixin.util.findMethods @@ -237,7 +237,7 @@ private fun copyAnnotation(modifiers: PsiModifierList, newModifiers: PsiModifier } inline fun disableAnnotationWrapping(project: Project, func: () -> Unit) { - if (!MinecraftSettings.instance.isShadowAnnotationsSameLine) { + if (!MinecraftProjectSettings.getInstance(project).isShadowAnnotationsSameLine) { func() return } diff --git a/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt b/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt index d8d5535a8..d3a5c35f0 100644 --- a/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt +++ b/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt @@ -39,6 +39,7 @@ class MixinCompletionConfidence : CompletionConfidence() { PsiJavaPatterns.psiAnnotation().qName( StandardPatterns.or( StandardPatterns.string().startsWith(MixinConstants.PACKAGE), + StandardPatterns.string().startsWith(MixinConstants.MixinExtras.PACKAGE), StandardPatterns.string() .oneOf(MixinAnnotationHandler.getBuiltinHandlers().map { it.first }.toList()), ) diff --git a/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt new file mode 100644 index 000000000..147b34023 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt @@ -0,0 +1,117 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.MixinModuleType +import com.demonwav.mcdev.platform.mixin.folding.MixinFoldingSettings +import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReference +import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.MemberReference +import com.intellij.lang.ASTNode +import com.intellij.lang.folding.CustomFoldingBuilder +import com.intellij.lang.folding.FoldingDescriptor +import com.intellij.openapi.editor.Document +import com.intellij.openapi.util.TextRange +import com.intellij.psi.JavaRecursiveElementWalkingVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiModifierList +import com.intellij.psi.util.PsiTreeUtil + +class MEDefinitionFoldingBuilder : CustomFoldingBuilder() { + override fun isDumbAware() = false + + override fun isRegionCollapsedByDefault(node: ASTNode): Boolean = + MixinFoldingSettings.instance.state.foldDefinitions + + override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String { + val psi = node.psi + if (psi is PsiLiteralExpression) { + val value = psi.value as? String ?: return "..." + val memberReference = MemberReference.parse(value) ?: return "..." + return memberReference.presentableText + } + return "..." + } + + override fun buildLanguageFoldRegions( + descriptors: MutableList, + root: PsiElement, + document: Document, + quick: Boolean + ) { + if (root !is PsiJavaFile || !MixinModuleType.isInModule(root)) { + return + } + + root.accept(Visitor(descriptors)) + } + + private class Visitor(private val descriptors: MutableList) : + JavaRecursiveElementWalkingVisitor() { + override fun visitModifierList(list: PsiModifierList) { + val currentDefinitionList = mutableListOf() + val definitionLists = mutableListOf>() + + for (annotation in list.annotations) { + if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + currentDefinitionList += annotation + } else if (currentDefinitionList.isNotEmpty()) { + definitionLists += currentDefinitionList.toList() + currentDefinitionList.clear() + } + } + + if (currentDefinitionList.isNotEmpty()) { + definitionLists += currentDefinitionList + } + + if (definitionLists.isEmpty()) { + return + } + + for (definitionList in definitionLists) { + val range = TextRange( + definitionList.first().parameterList.firstChild.nextSibling.textRange.startOffset, + PsiTreeUtil.getDeepestVisibleLast(definitionList.last())!!.textRange.startOffset, + ) + if (!range.isEmpty) { + descriptors.add(FoldingDescriptor(list.node, range)) + } + } + + super.visitModifierList(list) + } + + override fun visitLiteralExpression(expression: PsiLiteralExpression) { + if (FieldDefinitionReference.ELEMENT_PATTERN.accepts(expression) || + MethodDefinitionReference.ELEMENT_PATTERN.accepts(expression) + ) { + if (MemberReference.parse(expression.value as String) != null) { + descriptors.add(FoldingDescriptor(expression.node, expression.textRange)) + } + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt new file mode 100644 index 000000000..638bf6f13 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt @@ -0,0 +1,368 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBinaryExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBoundMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclaration +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEFreeMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MELitExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMemberAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENewExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStaticMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MESuperCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.findMultiInjectionHost +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.RemoveAnnotationQuickFix +import com.intellij.lang.annotation.AnnotationBuilder +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiModifierListOwner +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.TypeConversionUtil +import com.intellij.psi.util.parentOfType + +class MEExpressionAnnotator : Annotator { + override fun annotate(element: PsiElement, holder: AnnotationHolder) { + when (element) { + is MEDeclaration -> { + val parent = element.parent as? MEDeclarationItem ?: return + if (parent.isType) { + highlightDeclaration(holder, element, MEExpressionSyntaxHighlighter.IDENTIFIER_TYPE_DECLARATION) + } else { + highlightDeclaration(holder, element, MEExpressionSyntaxHighlighter.IDENTIFIER_DECLARATION) + } + } + is MEName -> { + if (!element.isWildcard) { + when (val parent = element.parent) { + is METype, + is MENewExpression -> highlightType(holder, element) + is MEMemberAccessExpression -> highlightVariable( + holder, + element, + MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME, + true, + ) + is MESuperCallExpression, + is MEMethodCallExpression, + is MEStaticMethodCallExpression, + is MEBoundMethodReferenceExpression, + is MEFreeMethodReferenceExpression -> highlightVariable( + holder, + element, + MEExpressionSyntaxHighlighter.IDENTIFIER_CALL, + false, + ) + is MENameExpression -> { + if (METypeUtil.isExpressionDirectlyInTypePosition(parent)) { + highlightType(holder, element) + } else { + highlightVariable( + holder, + element, + MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE, + false, + ) + } + } + else -> highlightType(holder, element) + } + } + } + is MELitExpression -> { + val minusToken = element.minusToken + if (minusToken != null) { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(minusToken) + .textAttributes(MEExpressionSyntaxHighlighter.NUMBER) + .create() + } + + if (!element.isNull && !element.isString && element.value == null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.invalid_number") + ) + .range(element) + .create() + } + } + is MEBinaryExpression -> { + val rightExpr = element.rightExpr + if (element.operator == MEExpressionTypes.TOKEN_INSTANCEOF && + rightExpr !is MENameExpression && + rightExpr !is MEArrayAccessExpression && + rightExpr != null + ) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.instanceof_non_type") + ) + .range(rightExpr) + .create() + } + } + is MEArrayAccessExpression -> { + if (METypeUtil.isExpressionDirectlyInTypePosition(element)) { + val indexExpr = element.indexExpr + if (indexExpr != null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.index_not_expected_in_type"), + ) + .range(indexExpr) + .create() + } + val arrayExpr = element.arrayExpr + if (arrayExpr !is MEArrayAccessExpression && arrayExpr !is MENameExpression) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.instanceof_non_type"), + ) + .range(arrayExpr) + .create() + } + } else if (element.indexExpr == null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.array_access_missing_index"), + ) + .range(element.leftBracketToken) + .create() + } + } + is MENewExpression -> { + if (element.isArrayCreation) { + val initializer = element.arrayInitializer + if (initializer != null) { + if (element.dimExprs.isNotEmpty()) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.new_array_dim_expr_with_initializer"), + ) + .range(initializer) + .create() + } else if (initializer.expressionList.isEmpty()) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.empty_array_initializer"), + ) + .range(initializer) + .create() + } + } else { + if (element.dimExprs.isEmpty()) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.missing_array_length") + ) + .range(element.dimExprTokens[0].leftBracket) + .create() + } else { + element.dimExprTokens.asSequence().dropWhile { it.expr != null }.forEach { + if (it.expr != null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.array_length_after_empty") + ) + .range(it.expr) + .create() + } + } + } + } + } else if (!element.hasConstructorArguments) { + val type = element.type + if (type != null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.new_no_constructor_args_or_array"), + ) + .range(type) + .create() + } + } + } + } + } + + private fun highlightDeclaration( + holder: AnnotationHolder, + declaration: MEDeclaration, + defaultColor: TextAttributesKey, + ) { + val isUnused = ReferencesSearch.search(declaration).findFirst() == null + + if (isUnused) { + val message = MCDevBundle("mixinextras.expression.lang.errors.unused_definition") + val annotation = holder.newAnnotation(HighlightSeverity.WARNING, message) + .range(declaration) + .highlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL) + + val containingAnnotation = declaration.findMultiInjectionHost()?.parentOfType()?.takeIf { + it.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) + } + if (containingAnnotation != null) { + val inspectionManager = InspectionManager.getInstance(containingAnnotation.project) + @Suppress("StatefulEp") // IntelliJ is wrong here + val fix = object : RemoveAnnotationQuickFix( + containingAnnotation, + containingAnnotation.parentOfType() + ) { + override fun getFamilyName() = MCDevBundle("mixinextras.expression.lang.errors.unused_symbol.fix") + } + val problemDescriptor = inspectionManager.createProblemDescriptor( + declaration, + message, + fix, + ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, + true + ) + annotation.newLocalQuickFix(fix, problemDescriptor).registerFix() + } + + annotation.create() + } else { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(declaration) + .textAttributes(defaultColor) + .create() + } + } + + private fun highlightType(holder: AnnotationHolder, type: MEName) { + val typeName = type.text + val isPrimitive = typeName != "void" && TypeConversionUtil.isPrimitive(typeName) + val isUnresolved = !isPrimitive && type.reference?.resolve() == null + + if (isUnresolved) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.unresolved_symbol") + ) + .range(type) + .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) + .withDefinitionFix(type) + .create() + } else { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(type) + .textAttributes( + if (isPrimitive) { + MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE + } else { + MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME + } + ) + .create() + } + } + + private fun highlightVariable( + holder: AnnotationHolder, + variable: MEName, + defaultColor: TextAttributesKey, + isMember: Boolean, + ) { + val variableName = variable.text + val isUnresolved = (variableName != "length" || !isMember) && variable.reference?.resolve() == null + + if (isUnresolved) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.unresolved_symbol") + ) + .range(variable) + .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) + .withDefinitionFix(variable) + .create() + } else { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(variable) + .textAttributes(defaultColor) + .create() + } + } + + private fun AnnotationBuilder.withDefinitionFix(name: MEName) = + withFix(AddDefinitionInspection(name)) + + private class AddDefinitionInspection(name: MEName) : LocalQuickFixAndIntentionActionOnPsiElement(name) { + private val id = name.text + + override fun getFamilyName(): String = "Add @Definition" + + override fun getText(): String = "$familyName(id = \"$id\")" + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + if (editor == null) { + MEExpressionCompletionUtil.addDefinition( + project, + startElement, + id, + "" + ) + return + } + val annotation = MEExpressionCompletionUtil.addDefinition( + project, + startElement, + id, + "dummy" + ) ?: return + val dummy = annotation.findAttribute("dummy") as? PsiElement ?: return + val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(editor) + hostEditor.caretModel.moveToOffset(dummy.textOffset) + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(hostEditor.document) + hostEditor.document.replaceString(dummy.textRange.startOffset, dummy.textRange.endOffset, "") + AutoPopupController.getInstance(project).autoPopupMemberLookup(hostEditor, null) + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt new file mode 100644 index 000000000..323856006 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt @@ -0,0 +1,41 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.intellij.lang.BracePair +import com.intellij.lang.PairedBraceMatcher +import com.intellij.psi.PsiFile +import com.intellij.psi.tree.IElementType + +class MEExpressionBraceMatcher : PairedBraceMatcher { + companion object { + private val PAIRS = arrayOf( + BracePair(MEExpressionTypes.TOKEN_LEFT_PAREN, MEExpressionTypes.TOKEN_RIGHT_PAREN, false), + BracePair(MEExpressionTypes.TOKEN_LEFT_BRACKET, MEExpressionTypes.TOKEN_RIGHT_BRACKET, false), + BracePair(MEExpressionTypes.TOKEN_LEFT_BRACE, MEExpressionTypes.TOKEN_RIGHT_BRACE, false), + ) + } + + override fun getPairs() = PAIRS + override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?) = true + override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int) = openingBraceOffset +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt new file mode 100644 index 000000000..964bd4a67 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt @@ -0,0 +1,153 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.PlatformAssets +import com.intellij.openapi.options.colors.AttributesDescriptor +import com.intellij.openapi.options.colors.ColorDescriptor +import com.intellij.openapi.options.colors.ColorSettingsPage + +class MEExpressionColorSettingsPage : ColorSettingsPage { + companion object { + private val DESCRIPTORS = arrayOf( + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.string.display_name"), + MEExpressionSyntaxHighlighter.STRING + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.string_escape.display_name"), + MEExpressionSyntaxHighlighter.STRING_ESCAPE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.number.display_name"), + MEExpressionSyntaxHighlighter.NUMBER + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.keyword.display_name"), + MEExpressionSyntaxHighlighter.KEYWORD + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.operator.display_name"), + MEExpressionSyntaxHighlighter.OPERATOR + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.parens.display_name"), + MEExpressionSyntaxHighlighter.PARENS + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.brackets.display_name"), + MEExpressionSyntaxHighlighter.BRACKETS + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.braces.display_name"), + MEExpressionSyntaxHighlighter.BRACES + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.dot.display_name"), + MEExpressionSyntaxHighlighter.DOT + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.method_reference.display_name"), + MEExpressionSyntaxHighlighter.METHOD_REFERENCE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.comma.display_name"), + MEExpressionSyntaxHighlighter.COMMA + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.capture.display_name"), + MEExpressionSyntaxHighlighter.CAPTURE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.wildcard.display_name"), + MEExpressionSyntaxHighlighter.WILDCARD + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.call_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_CALL + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.class_name_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.primitive_type_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.member_name_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.variable_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE + ), + AttributesDescriptor( + MCDevBundle.pointer( + "mixinextras.expression.lang.highlighting.type_declaration_identifier.display_name" + ), + MEExpressionSyntaxHighlighter.IDENTIFIER_TYPE_DECLARATION + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.declaration_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_DECLARATION + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.bad_char.display_name"), + MEExpressionSyntaxHighlighter.BAD_CHAR + ), + ) + + private val TAGS = mapOf( + "call" to MEExpressionSyntaxHighlighter.IDENTIFIER_CALL, + "class_name" to MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME, + "member_name" to MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME, + "primitive_type" to MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE, + "variable" to MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE, + ) + } + + override fun getIcon() = PlatformAssets.MIXIN_ICON + override fun getHighlighter() = MEExpressionSyntaxHighlighter() + + override fun getDemoText() = """ + variable.function( + 'a string with \\ escapes', + 123 + @(45), + ?, + ClassName.class, + foo.bar, + new int[] { 1, 2, 3 }, + method::reference, + 'a bad character: ' # other_identifier + )[0] + """.trimIndent() + + override fun getAdditionalHighlightingTagToDescriptorMap() = TAGS + override fun getAttributeDescriptors() = DESCRIPTORS + override fun getColorDescriptors(): Array = ColorDescriptor.EMPTY_ARRAY + override fun getDisplayName() = MCDevBundle("mixinextras.expression.lang.display_name") +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt new file mode 100644 index 000000000..339634a39 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt @@ -0,0 +1,138 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.intellij.codeInsight.TailType +import com.intellij.codeInsight.completion.BasicExpressionCompletionContributor +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.TailTypeDecorator +import com.intellij.util.ProcessingContext + +class MEExpressionCompletionContributor : CompletionContributor() { + init { + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.STATEMENT_KEYWORD_PLACE, + KeywordCompletionProvider( + Keyword("return", TailType.INSERT_SPACE), + Keyword("throw", TailType.INSERT_SPACE), + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.VALUE_KEYWORD_PLACE, + KeywordCompletionProvider( + Keyword("this"), + Keyword("super"), + Keyword("true"), + Keyword("false"), + Keyword("null"), + Keyword("new", TailType.INSERT_SPACE), + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.CLASS_PLACE, + KeywordCompletionProvider( + Keyword("class") + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.INSTANCEOF_PLACE, + KeywordCompletionProvider( + Keyword("instanceof", TailType.INSERT_SPACE) + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.METHOD_REFERENCE_PLACE, + KeywordCompletionProvider( + Keyword("new") + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.STRING_LITERAL_PLACE, + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + result.addAllElements( + MEExpressionCompletionUtil.getStringCompletions( + parameters.originalFile.project, + parameters.position + ) + ) + } + } + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.FROM_BYTECODE_PLACE, + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val project = parameters.originalFile.project + result.addAllElements( + MEExpressionCompletionUtil.getCompletionVariantsFromBytecode(project, parameters.position) + ) + } + } + ) + } + + private class KeywordCompletionProvider( + private vararg val keywords: Keyword, + ) : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + result.addAllElements( + keywords.map { keyword -> + var lookupItem = + BasicExpressionCompletionContributor.createKeywordLookupItem(parameters.position, keyword.name) + if (keyword.tailType != TailType.NONE) { + lookupItem = object : TailTypeDecorator(lookupItem) { + override fun computeTailType(context: InsertionContext?) = keyword.tailType + } + } + lookupItem + } + ) + } + } + + private class Keyword(val name: String, val tailType: TailType = TailType.NONE) +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt new file mode 100644 index 000000000..c172b57d6 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt @@ -0,0 +1,1339 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.MinecraftProjectSettings +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil.virtualInsn +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil.virtualInsnOrNull +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEAssignStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBoundMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECapturingExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECastExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEClassConstantExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEFreeMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MELitExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMemberAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENewExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatementItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStaticMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MESuperCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement +import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.MERecursiveWalkingVisitor +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil.notInTypePosition +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil.validType +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.util.AsmDfaUtil +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.SignatureToPsi +import com.demonwav.mcdev.platform.mixin.util.canonicalName +import com.demonwav.mcdev.platform.mixin.util.hasAccess +import com.demonwav.mcdev.platform.mixin.util.isPrimitive +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.platform.mixin.util.textify +import com.demonwav.mcdev.platform.mixin.util.toPsiType +import com.demonwav.mcdev.util.BeforeOrAfter +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findContainingNameValuePair +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.findMultiInjectionHost +import com.demonwav.mcdev.util.invokeLater +import com.demonwav.mcdev.util.mapFirstNotNull +import com.demonwav.mcdev.util.packageName +import com.intellij.codeInsight.TailType +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.folding.CodeFoldingManager +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.lookup.TailTypeDecorator +import com.intellij.codeInsight.template.Expression +import com.intellij.codeInsight.template.Template +import com.intellij.codeInsight.template.TemplateBuilderImpl +import com.intellij.codeInsight.template.TemplateEditingAdapter +import com.intellij.codeInsight.template.TemplateManager +import com.intellij.codeInsight.template.TextResult +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.FoldRegion +import com.intellij.openapi.project.Project +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiAnonymousClass +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiModifierList +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.createSmartPointer +import com.intellij.psi.util.parentOfType +import com.intellij.psi.util.parents +import com.intellij.util.PlatformIcons +import com.intellij.util.text.CharArrayUtil +import com.llamalad7.mixinextras.expression.impl.flow.ComplexFlowValue +import com.llamalad7.mixinextras.expression.impl.flow.DummyFlowValue +import com.llamalad7.mixinextras.expression.impl.flow.FlowValue +import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander +import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.InstantiationInfo +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.pool.IdentifierPool +import com.llamalad7.mixinextras.expression.impl.utils.FlowDecorations +import org.apache.commons.lang3.mutable.MutableInt +import org.objectweb.asm.Handle +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.signature.SignatureReader +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.IincInsnNode +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.IntInsnNode +import org.objectweb.asm.tree.InvokeDynamicInsnNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.MultiANewArrayInsnNode +import org.objectweb.asm.tree.TypeInsnNode +import org.objectweb.asm.tree.VarInsnNode + +private typealias TemplateExpressionContext = com.intellij.codeInsight.template.ExpressionContext + +object MEExpressionCompletionUtil { + private const val DEBUG_COMPLETION = false + + private val NORMAL_ELEMENT = PlatformPatterns.psiElement() + .inside(MEStatement::class.java) + .andNot(PlatformPatterns.psiElement().inside(MELitExpression::class.java)) + .notInTypePosition() + private val TYPE_PATTERN = PlatformPatterns.psiElement() + .inside(MEStatement::class.java) + .validType() + private val AFTER_END_EXPRESSION_PATTERN = StandardPatterns.or( + PlatformPatterns.psiElement().afterLeaf( + PlatformPatterns.psiElement().withElementType( + TokenSet.create( + MEExpressionTypes.TOKEN_IDENTIFIER, + MEExpressionTypes.TOKEN_WILDCARD, + MEExpressionTypes.TOKEN_RIGHT_PAREN, + MEExpressionTypes.TOKEN_RIGHT_BRACKET, + MEExpressionTypes.TOKEN_RIGHT_BRACE, + MEExpressionTypes.TOKEN_BOOL_LIT, + MEExpressionTypes.TOKEN_CLASS, + MEExpressionTypes.TOKEN_INT_LIT, + MEExpressionTypes.TOKEN_DEC_LIT, + MEExpressionTypes.TOKEN_NULL_LIT, + MEExpressionTypes.TOKEN_STRING_TERMINATOR, + ) + ) + ), + PlatformPatterns.psiElement().afterLeaf(PlatformPatterns.psiElement().withText("new").afterLeaf("::")), + ) + + val STATEMENT_KEYWORD_PLACE = PlatformPatterns.psiElement().afterLeaf( + PlatformPatterns.psiElement().withText("{").withParent(MEStatementItem::class.java) + ) + val VALUE_KEYWORD_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + StandardPatterns.not(AFTER_END_EXPRESSION_PATTERN), + StandardPatterns.not(PlatformPatterns.psiElement().afterLeaf(".")), + StandardPatterns.not(PlatformPatterns.psiElement().afterLeaf("::")), + ) + val CLASS_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + PlatformPatterns.psiElement() + .afterLeaf( + PlatformPatterns.psiElement().withText(".") + .withParent(PlatformPatterns.psiElement().withFirstChild(TYPE_PATTERN)) + ), + ) + val INSTANCEOF_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + AFTER_END_EXPRESSION_PATTERN, + ) + val METHOD_REFERENCE_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + PlatformPatterns.psiElement().afterLeaf("::"), + ) + val STRING_LITERAL_PLACE = PlatformPatterns.psiElement().withElementType( + TokenSet.create(MEExpressionTypes.TOKEN_STRING, MEExpressionTypes.TOKEN_STRING_TERMINATOR) + ) + val FROM_BYTECODE_PLACE = PlatformPatterns.psiElement() + .inside(MEStatement::class.java) + .andNot(PlatformPatterns.psiElement().inside(MELitExpression::class.java)) + + private val DOT_CLASS_TAIL = object : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, ".class") + return moveCaret(editor, tailOffset, 6) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val dotOffset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + if (!CharArrayUtil.regionMatches(chars, dotOffset, ".")) { + return true + } + val classOffset = CharArrayUtil.shiftForward(chars, dotOffset + 1, " \n\t") + return !CharArrayUtil.regionMatches(chars, classOffset, "class") + } + } + + private val COLON_COLON_NEW_TAIL = object : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, "::new") + return moveCaret(editor, tailOffset, 5) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val colonColonOffset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + if (!CharArrayUtil.regionMatches(chars, colonColonOffset, "::")) { + return true + } + val newOffset = CharArrayUtil.shiftForward(chars, colonColonOffset + 2, " \n\t") + return !CharArrayUtil.regionMatches(chars, newOffset, "new") + } + } + + fun getStringCompletions(project: Project, contextElement: PsiElement): List { + val expressionAnnotation = contextElement.findMultiInjectionHost()?.parentOfType() + ?: return emptyList() + if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + return emptyList() + } + + val modifierList = expressionAnnotation.findContainingModifierList() ?: return emptyList() + + val (handler, handlerAnnotation) = modifierList.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null + handler to annotation + } ?: return emptyList() + + return handler.resolveTarget(handlerAnnotation).flatMap { + (it as? MethodTargetMember)?.classAndMethod?.method?.instructions?.mapNotNull { insn -> + if (insn is LdcInsnNode && insn.cst is String) { + LookupElementBuilder.create(insn.cst) + } else { + null + } + } ?: emptyList() + } + } + + fun getCompletionVariantsFromBytecode(project: Project, contextElement: PsiElement): List { + val statement = contextElement.parentOfType() ?: return emptyList() + + val expressionAnnotation = contextElement.findMultiInjectionHost()?.parentOfType() + ?: return emptyList() + if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + return emptyList() + } + + val modifierList = expressionAnnotation.findContainingModifierList() ?: return emptyList() + val module = modifierList.findModule() ?: return emptyList() + + val mixinClass = modifierList.findContainingClass() ?: return emptyList() + + val (handler, handlerAnnotation) = modifierList.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null + handler to annotation + } ?: return emptyList() + + val cursorOffset = contextElement.textRange.startOffset - statement.textRange.startOffset + + return mixinClass.mixinTargets.flatMap { targetClass -> + val poolFactory = MEExpressionMatchUtil.createIdentifierPoolFactory(module, targetClass, modifierList) + handler.resolveTarget(handlerAnnotation, targetClass) + .filterIsInstance() + .flatMap { methodTarget -> + getCompletionVariantsFromBytecode( + project, + mixinClass, + cursorOffset, + statement.copy() as MEStatement, + targetClass, + methodTarget.classAndMethod.method, + poolFactory, + ) + } + } + } + + private fun getCompletionVariantsFromBytecode( + project: Project, + mixinClass: PsiClass, + cursorOffsetIn: Int, + statement: MEStatement, + targetClass: ClassNode, + targetMethod: MethodNode, + poolFactory: IdentifierPoolFactory, + ): List { + /* + * MixinExtras isn't designed to match against incomplete expressions, which is what we need to do to produce + * completion options. The only support there is, is to match incomplete parameter lists and so on + * ("list inputs" to expressions). What follows is a kind of DIY match where we figure out different options + * for what the user might be trying to complete and hand it to MixinExtras to do the actual matching. Note that + * IntelliJ already inserts an identifier at the caret position to make auto-completion easier. + * + * We have four classes of problems to solve here: + * 1. There may already be a capture in the expression causing MixinExtras to return the wrong instructions. + * 2. There may be unresolved identifiers in the expression, causing MixinExtras to match nothing, which isn't + * ideal. + * 3. "this." expands to a field access, but the user may be trying to complete a method call (and other + * similar situations). + * 4. What the user is typing may form only a subexpression of a larger expression. For example, with + * "foo()", the user may actually be trying to type the expression "foo(x + y) + z". That is, "x", + * which is where the caret is, may not be a direct subexpression to the "foo" call expression, which itself + * may not be a direct subexpression of its parent. + * + * Throughout this process, we have to keep careful track of where the caret is, because: + * 1. As we make changes to the expression to the left of the caret, the caret may shift. + * 2. As we make copies of the element, or entirely new elements, that new element's textOffset may be different + * from the original one. + */ + + if (DEBUG_COMPLETION) { + println("======") + println(targetMethod.textify()) + println("======") + } + + if (targetMethod.instructions == null) { + return emptyList() + } + + val cursorOffset = MutableInt(cursorOffsetIn) + val pool = poolFactory(targetMethod) + val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, targetMethod) ?: return emptyList() + + // Removing all explicit captures from the expression solves problem 1 (see comment above). + removeExplicitCaptures(statement, cursorOffset) + // Replacing unresolved names with wildcards solves problem 2 (see comment above). + replaceUnresolvedNamesWithWildcards(project, statement, cursorOffset, pool) + + val elementAtCursor = statement.findElementAt(cursorOffset.toInt()) ?: return emptyList() + + /* + * To solve problem 4 (see comment above), we first find matches for the top level statement, ignoring the + * subexpression that the caret is on. Then we iterate down into the subexpression that contains the caret and + * match that against all the statement's input flows in the same way as we matched the statement against all + * the instructions in the target method. Then we keep iterating until we reach the identifier the caret is on. + */ + + // Replace the subexpression the caret is on with a wildcard expression, so MixinExtras ignores it. + val wildcardReplacedStatement = statement.copy() as MEStatement + var cursorOffsetInCopyFile = + cursorOffset.toInt() - statement.textRange.startOffset + wildcardReplacedStatement.textRange.startOffset + replaceCursorInputWithWildcard(project, wildcardReplacedStatement, cursorOffsetInCopyFile) + + // Iterate through possible "variants" of the statement that the user may be trying to complete; it doesn't + // matter if they don't parse, then we just skip them. This solves problem 3 (see comment above). + var matchingFlows = mutableListOf() + for (statementToMatch in getStatementVariants(project.meExpressionElementFactory, wildcardReplacedStatement)) { + if (DEBUG_COMPLETION) { + println("Matching against statement ${statementToMatch.text}") + } + + val meStatement = MEExpressionMatchUtil.createExpression(statementToMatch.text) ?: continue + MEExpressionMatchUtil.findMatchingInstructions( + targetClass, + targetMethod, + pool, + flows, + meStatement, + flows.keys, + ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // use most permissive type for completion + true, + ) { match -> + matchingFlows += match.flow + if (DEBUG_COMPLETION) { + println("Matched ${match.flow.virtualInsnOrNull?.insn?.textify()}") + } + } + } + if (matchingFlows.isEmpty()) { + return emptyList() + } + + // Iterate through subexpressions until we reach the identifier the caret is on + var roundNumber = 0 + var subExpr: MEMatchableElement = statement + while (true) { + // Replace the subexpression the caret is on with a wildcard expression, so MixinExtras ignores it. + val inputExprOnCursor = subExpr.getInputExprs().firstOrNull { it.textRange.contains(cursorOffset.toInt()) } + ?: break + val wildcardReplacedExpr = inputExprOnCursor.copy() as MEExpression + cursorOffsetInCopyFile = cursorOffset.toInt() - + inputExprOnCursor.textRange.startOffset + wildcardReplacedExpr.textRange.startOffset + + if (DEBUG_COMPLETION) { + val exprText = wildcardReplacedExpr.text + val cursorOffsetInExpr = cursorOffsetInCopyFile - wildcardReplacedExpr.textRange.startOffset + val exprWithCaretMarker = when { + cursorOffsetInExpr < 0 -> "$exprText" + cursorOffsetInExpr > exprText.length -> "$exprText" + else -> exprText.replaceRange(cursorOffsetInExpr, cursorOffsetInExpr, "") + } + println("=== Round ${++roundNumber}: handling $exprWithCaretMarker") + } + + replaceCursorInputWithWildcard(project, wildcardReplacedExpr, cursorOffsetInCopyFile) + + // Iterate through the possible "varaints" of the expression in the same way as we did for the statement + // above. This solves problem 3 (see comment above). + val newMatchingFlows = mutableSetOf() + for (exprToMatch in getExpressionVariants(project.meExpressionElementFactory, wildcardReplacedExpr)) { + if (DEBUG_COMPLETION) { + println("Matching against expression ${exprToMatch.text}") + } + + val meExpression = MEExpressionMatchUtil.createExpression(exprToMatch.text) ?: continue + + val flattenedInstructions = mutableSetOf() + for (flow in matchingFlows) { + getInstructionsInFlowTree( + flow, + flattenedInstructions, + subExpr !is MEExpressionStatement && subExpr !is MEParenthesizedExpression + ) + } + + MEExpressionMatchUtil.findMatchingInstructions( + targetClass, + targetMethod, + pool, + flows, + meExpression, + flattenedInstructions.map { it.insn }, + ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // use most permissive type for completion + true, + ) { match -> + newMatchingFlows += match.flow + if (DEBUG_COMPLETION) { + println("Matched ${match.flow.virtualInsnOrNull?.insn?.textify()}") + } + } + } + + if (newMatchingFlows.isEmpty()) { + return emptyList() + } + matchingFlows = newMatchingFlows.toMutableList() + + subExpr = inputExprOnCursor + } + + val cursorInstructions = mutableSetOf() + for (flow in matchingFlows) { + getInstructionsInFlowTree(flow, cursorInstructions, false) + } + + if (DEBUG_COMPLETION) { + println("Found ${cursorInstructions.size} matching instructions:") + for (insn in cursorInstructions) { + println("- ${insn.insn.insn.textify()}") + } + } + + // Try to decide if we should be completing types or normal expressions. + // Not as easy as it sounds (think incomplete casts looking like parenthesized expressions). + // Note that it's possible to complete types and expressions at the same time. + val isInsideMeType = PsiTreeUtil.getParentOfType( + elementAtCursor, + METype::class.java, + false, + MEExpression::class.java + ) != null + val isInsideNewExpr = PsiTreeUtil.getParentOfType( + elementAtCursor, + MENewExpression::class.java, + false, + MEExpression::class.java + ) != null + val cursorExprInTypePosition = !isInsideMeType && + elementAtCursor.parentOfType()?.let(METypeUtil::isExpressionInTypePosition) == true + val inTypePosition = isInsideMeType || isInsideNewExpr || cursorExprInTypePosition + val isPossiblyIncompleteCast = !inTypePosition && + elementAtCursor.parentOfType() + ?.parents(false) + ?.dropWhile { it is MEArrayAccessExpression && it.indexExpr == null } + ?.firstOrNull() is MEParenthesizedExpression + val canCompleteExprs = !inTypePosition + val canCompleteTypes = inTypePosition || isPossiblyIncompleteCast + + if (DEBUG_COMPLETION) { + println("canCompleteExprs = $canCompleteExprs") + println("canCompleteTypes = $canCompleteTypes") + } + + val eliminableResults = cursorInstructions.flatMap { insn -> + getCompletionsForInstruction( + project, + targetClass, + targetMethod, + insn.insn, + insn.originalInsn, + flows, + mixinClass, + canCompleteExprs, + canCompleteTypes + ) + } + + // In the case of multiple instructions producing the same lookup, attempt to show only the "best" lookup. + // For example, if a local variable is only sometimes able to be targeted using implicit ordinals in this + // expression, prefer specifying the ordinal. + return eliminableResults.groupBy { it.uniquenessKey }.values.map { it.max().lookupElement } + } + + private fun replaceUnresolvedNamesWithWildcards( + project: Project, + statement: MEStatement, + cursorOffset: MutableInt, + pool: IdentifierPool, + ) { + val unresolvedNames = mutableListOf() + statement.accept(object : MERecursiveWalkingVisitor() { + override fun visitType(o: METype) { + val name = o.meName + if (!name.isWildcard && !pool.typeExists(name.text)) { + unresolvedNames += name + } + } + + override fun visitNameExpression(o: MENameExpression) { + val name = o.meName + if (!name.isWildcard) { + if (METypeUtil.isExpressionDirectlyInTypePosition(o)) { + if (!pool.typeExists(name.text)) { + unresolvedNames += name + } + } else { + if (!pool.memberExists(name.text)) { + unresolvedNames += name + } + } + } + } + + override fun visitSuperCallExpression(o: MESuperCallExpression) { + val name = o.memberName + if (name != null && !name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitSuperCallExpression(o) + } + + override fun visitMethodCallExpression(o: MEMethodCallExpression) { + val name = o.memberName + if (!name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitMethodCallExpression(o) + } + + override fun visitStaticMethodCallExpression(o: MEStaticMethodCallExpression) { + val name = o.memberName + if (!name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitStaticMethodCallExpression(o) + } + + override fun visitBoundMethodReferenceExpression(o: MEBoundMethodReferenceExpression) { + val name = o.memberName + if (name != null && !name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitBoundMethodReferenceExpression(o) + } + + override fun visitFreeMethodReferenceExpression(o: MEFreeMethodReferenceExpression) { + val name = o.memberName + if (name != null && !name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitFreeMethodReferenceExpression(o) + } + + override fun visitMemberAccessExpression(o: MEMemberAccessExpression) { + val name = o.memberName + if (!name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitMemberAccessExpression(o) + } + + override fun visitNewExpression(o: MENewExpression) { + val name = o.type + if (name != null && !name.isWildcard && !pool.typeExists(name.text)) { + unresolvedNames += name + } + super.visitNewExpression(o) + } + }) + + for (unresolvedName in unresolvedNames) { + val startOffset = unresolvedName.textRange.startOffset + if (cursorOffset.toInt() > startOffset) { + cursorOffset.setValue(cursorOffset.toInt() - unresolvedName.textLength + 1) + } + + unresolvedName.replace(project.meExpressionElementFactory.createName("?")) + } + } + + private fun removeExplicitCaptures(statement: MEStatement, cursorOffset: MutableInt) { + val captures = mutableListOf() + + statement.accept(object : MERecursiveWalkingVisitor() { + override fun elementFinished(element: PsiElement) { + // do this on elementFinished to ensure that inner captures are replaced before outer captures + if (element is MECapturingExpression) { + captures += element + } + } + }) + + for (capture in captures) { + val innerExpr = capture.expression ?: continue + val textRange = capture.textRange + + if (cursorOffset.toInt() > textRange.startOffset) { + cursorOffset.setValue(cursorOffset.toInt() - if (cursorOffset.toInt() >= textRange.endOffset) 3 else 2) + } + + capture.replace(innerExpr) + } + } + + private fun replaceCursorInputWithWildcard(project: Project, element: MEMatchableElement, cursorOffset: Int) { + for (input in element.getInputExprs()) { + if (input.textRange.contains(cursorOffset)) { + input.replace(project.meExpressionElementFactory.createExpression("?")) + return + } + } + } + + private fun getInstructionsInFlowTree( + flow: FlowValue, + outInstructions: MutableSet, + strict: Boolean, + ) { + if (flow is DummyFlowValue || flow is ComplexFlowValue) { + return + } + + if (!strict) { + val originalInsn = InsnExpander.getRepresentative(flow) ?: flow.insn + if (!outInstructions.add(ExpandedInstruction(flow.virtualInsn, originalInsn))) { + return + } + } + for (i in 0 until flow.inputCount()) { + getInstructionsInFlowTree(flow.getInput(i), outInstructions, false) + } + } + + private fun getCompletionsForInstruction( + project: Project, + targetClass: ClassNode, + targetMethod: MethodNode, + insn: VirtualInsn, + originalInsn: AbstractInsnNode, + flows: FlowMap, + mixinClass: PsiClass, + canCompleteExprs: Boolean, + canCompleteTypes: Boolean + ): List { + val flow = flows[insn] + when (insn.insn) { + is LdcInsnNode -> { + when (val cst = insn.insn.cst) { + is Type -> { + if (canCompleteExprs && cst.isAccessibleFrom(mixinClass)) { + return listOf( + createTypeLookup(cst) + .withTailText(".class") + .withTail(DOT_CLASS_TAIL) + .createEliminable("class ${insn.insn.cst}") + ) + } + } + } + } + is VarInsnNode -> return createLocalVariableLookups( + project, + targetClass, + targetMethod, + originalInsn, + insn.insn.`var`, + insn.insn.opcode in Opcodes.ISTORE..Opcodes.ASTORE, + mixinClass + ) + is IincInsnNode -> return createLocalVariableLookups( + project, + targetClass, + targetMethod, + originalInsn, + insn.insn.`var`, + false, + mixinClass + ) + is FieldInsnNode -> { + if (canCompleteExprs) { + val definitionValue = "field = \"L${insn.insn.owner};${insn.insn.name}:${insn.insn.desc}\"" + var lookup = createUniqueLookup(insn.insn.name.toValidIdentifier()) + .withIcon(PlatformIcons.FIELD_ICON) + .withPresentableText(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + .withTypeText(Type.getType(insn.insn.desc).presentableName()) + .withDefinitionAndFold(insn.insn.name.toValidIdentifier(), "field", definitionValue) + if (insn.insn.opcode == Opcodes.GETSTATIC || insn.insn.opcode == Opcodes.PUTSTATIC) { + lookup = lookup.withLookupString(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + } + return listOf( + lookup.createEliminable("field ${insn.insn.owner}.${insn.insn.name}:${insn.insn.desc}") + ) + } + } + is MethodInsnNode -> { + if (canCompleteExprs) { + val definitionValue = "method = \"L${insn.insn.owner};${insn.insn.name}${insn.insn.desc}\"" + var lookup = createUniqueLookup(insn.insn.name.toValidIdentifier()) + .withIcon(PlatformIcons.METHOD_ICON) + .withPresentableText(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + .withDescTailText(insn.insn.desc) + .withTypeText(Type.getReturnType(insn.insn.desc).presentableName()) + .withDefinitionAndFold(insn.insn.name.toValidIdentifier(), "method", definitionValue) + if (insn.insn.opcode == Opcodes.INVOKESTATIC) { + lookup = lookup.withLookupString(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + } + return listOf( + lookup.withTail(ParenthesesTailType(!insn.insn.desc.startsWith("()"))) + .createEliminable("invoke ${insn.insn.owner}.${insn.insn.name}${insn.insn.desc}") + ) + } + } + is TypeInsnNode -> { + val type = Type.getObjectType(insn.insn.desc) + if (canCompleteTypes && type.isAccessibleFrom(mixinClass)) { + val lookup = createTypeLookup(type) + when (insn.insn.opcode) { + Opcodes.ANEWARRAY -> { + val arrayType = Type.getType('[' + Type.getObjectType(insn.insn.desc).descriptor) + return createNewArrayCompletion(flow, arrayType) + } + Opcodes.NEW -> { + val initCall = flow + ?.getDecoration(FlowDecorations.INSTANTIATION_INFO) + ?.initCall + ?.virtualInsnOrNull + ?.insn as? MethodInsnNode + ?: return emptyList() + return listOf( + lookup + .withDescTailText(initCall.desc) + .withTail(ParenthesesTailType(!initCall.desc.startsWith("()"))) + .createEliminable("new ${insn.insn.desc}${initCall.desc}") + ) + } + else -> return listOf(lookup.createEliminable("type ${insn.insn.desc}")) + } + } + } + is IntInsnNode -> { + if (insn.insn.opcode == Opcodes.NEWARRAY) { + if (canCompleteTypes) { + val arrayType = Type.getType( + when (insn.insn.operand) { + Opcodes.T_BOOLEAN -> "[B" + Opcodes.T_CHAR -> "[C" + Opcodes.T_FLOAT -> "[F" + Opcodes.T_DOUBLE -> "[D" + Opcodes.T_BYTE -> "[B" + Opcodes.T_SHORT -> "[S" + Opcodes.T_INT -> "[I" + Opcodes.T_LONG -> "[J" + else -> "[Lnull;" // wtf? + } + ) + return createNewArrayCompletion(flow, arrayType) + } + } + } + is MultiANewArrayInsnNode -> { + if (canCompleteTypes) { + val arrayType = Type.getType(insn.insn.desc) + return createNewArrayCompletion(flow, arrayType) + } + } + is InsnNode -> { + when (insn.insn.opcode) { + Opcodes.ARRAYLENGTH -> { + if (canCompleteExprs) { + return listOf( + createUniqueLookup("length") + .withIcon(PlatformIcons.FIELD_ICON) + .withTypeText("int") + .createEliminable("arraylength") + ) + } + } + } + } + is InvokeDynamicInsnNode -> { + if (insn.insn.bsm.owner == "java/lang/invoke/LambdaMetafactory") { + if (!canCompleteExprs) { + return emptyList() + } + + val handle = insn.insn.bsmArgs.getOrNull(1) as? Handle ?: return emptyList() + val definitionValue = "method = \"L${handle.owner};${handle.name}${handle.desc}\"" + if (handle.tag !in Opcodes.H_INVOKEVIRTUAL..Opcodes.H_INVOKEINTERFACE) { + return emptyList() + } + if (handle.tag == Opcodes.H_NEWINVOKESPECIAL) { + return listOf( + createTypeLookup(Type.getObjectType(handle.owner)) + .withTailText("::new") + .withTail(COLON_COLON_NEW_TAIL) + .createEliminable("constructorRef ${handle.owner}") + ) + } else { + return listOf( + createUniqueLookup(handle.name.toValidIdentifier()) + .withIcon(PlatformIcons.METHOD_ICON) + .withPresentableText(handle.owner.substringAfterLast('/') + "." + handle.name) + .withTypeText(Type.getReturnType(handle.desc).presentableName()) + .withDefinitionAndFold(handle.name.toValidIdentifier(), "method", definitionValue) + .createEliminable("methodRef ${handle.owner}.${handle.name}${handle.desc}") + ) + } + } + } + } + + return emptyList() + } + + private fun Type.typeNameToInsert(): String { + if (sort == Type.ARRAY) { + return elementType.typeNameToInsert() + "[]".repeat(dimensions) + } + if (sort != Type.OBJECT) { + return className + } + + val simpleName = internalName.substringAfterLast('/') + val lastValidCharIndex = (simpleName.length - 1 downTo 0).firstOrNull { + MEPsiUtil.isIdentifierStart(simpleName[it]) + } ?: return "_" + simpleName.filterInvalidIdentifierChars() + + return simpleName.substring(simpleName.lastIndexOf('$', lastValidCharIndex) + 1).toValidIdentifier() + } + + private fun String.toValidIdentifier(): String { + return when { + isEmpty() -> "_" + !MEPsiUtil.isIdentifierStart(this[0]) -> "_" + filterInvalidIdentifierChars() + else -> this[0] + substring(1).filterInvalidIdentifierChars() + } + } + + private fun String.filterInvalidIdentifierChars(): String { + return asSequence().joinToString("") { + if (MEPsiUtil.isIdentifierPart(it)) it.toString() else "_" + } + } + + private fun Type.presentableName(): String = when (sort) { + Type.ARRAY -> elementType.presentableName() + "[]".repeat(dimensions) + Type.OBJECT -> internalName.substringAfterLast('/') + else -> className + } + + private fun Type.isAccessibleFrom(fromClass: PsiClass): Boolean { + return when (sort) { + Type.ARRAY -> elementType.isAccessibleFrom(fromClass) + Type.OBJECT -> { + val facade = JavaPsiFacade.getInstance(fromClass.project) + val clazz = facade.findClass(canonicalName, fromClass.resolveScope) ?: return false + val pkg = fromClass.packageName?.let(facade::findPackage) ?: return false + clazz !is PsiAnonymousClass && PsiUtil.isAccessibleFromPackage(clazz, pkg) + } + else -> true + } + } + + private fun createTypeLookup(type: Type): LookupElementBuilder { + val definitionId = type.typeNameToInsert() + + val lookupElement = createUniqueLookup(definitionId) + .withIcon(PlatformIcons.CLASS_ICON) + .withPresentableText(type.presentableName()) + + return if (type.isPrimitive) { + lookupElement + } else { + lookupElement.withDefinition(definitionId, "type = ${type.canonicalName}.class") + } + } + + private fun createNewArrayCompletion(flow: FlowValue?, arrayType: Type): List { + val hasInitializer = flow?.hasDecoration(FlowDecorations.ARRAY_CREATION_INFO) == true + val initializerText = if (hasInitializer) "{}" else "" + return listOf( + createTypeLookup(arrayType.elementType) + .withTailText("[]".repeat(arrayType.dimensions) + initializerText) + .withTail( + BracketsTailType( + arrayType.dimensions, + hasInitializer, + ) + ) + .createEliminable("new ${arrayType.descriptor}$initializerText") + ) + } + + private fun createLocalVariableLookups( + project: Project, + targetClass: ClassNode, + targetMethod: MethodNode, + originalInsn: AbstractInsnNode, + index: Int, + isStore: Boolean, + mixinClass: PsiClass, + ): List { + // ignore "this" + if (!targetMethod.hasAccess(Opcodes.ACC_STATIC) && index == 0) { + return emptyList() + } + + var argumentsSize = Type.getArgumentsAndReturnSizes(targetMethod.desc) shr 2 + if (targetMethod.hasAccess(Opcodes.ACC_STATIC)) { + argumentsSize-- + } + val isArgsOnly = index < argumentsSize + + if (targetMethod.localVariables != null) { + val localsHere = targetMethod.localVariables.filter { localVariable -> + val firstValidInstruction = if (isStore) { + generateSequence(localVariable.start) { it.previous } + .firstOrNull { it.opcode >= 0 } + } else { + localVariable.start.next + } + if (firstValidInstruction == null) { + return@filter false + } + val validRange = targetMethod.instructions.indexOf(firstValidInstruction) until + targetMethod.instructions.indexOf(localVariable.end) + targetMethod.instructions.indexOf(originalInsn) in validRange + } + val locals = localsHere.filter { it.index == index } + + val elementFactory = JavaPsiFacade.getElementFactory(project) + + return locals.map { localVariable -> + val localPsiType = if (localVariable.signature != null) { + val sigToPsi = SignatureToPsi(elementFactory, mixinClass) + SignatureReader(localVariable.signature).acceptType(sigToPsi) + sigToPsi.type + } else { + Type.getType(localVariable.desc).toPsiType(elementFactory, mixinClass) + } + val localsOfMyType = localsHere.filter { it.desc == localVariable.desc } + val ordinal = localsOfMyType.indexOf(localVariable) + val isImplicit = localsOfMyType.size == 1 + val localName = localVariable.name.toValidIdentifier() + createUniqueLookup(localName) + .withIcon(PlatformIcons.VARIABLE_ICON) + .withTypeText(localPsiType.presentableText) + .withLocalDefinition( + localName, + Type.getType(localVariable.desc), + ordinal, + isArgsOnly, + isImplicit, + mixinClass, + ) + .createEliminable("local $localName", if (isImplicit) -1 else 0) + } + } + + // fallback to ASM dataflow + val localTypes = AsmDfaUtil.getLocalVariableTypes(project, targetClass, targetMethod, originalInsn) + ?: return emptyList() + val localType = localTypes.getOrNull(index) ?: return emptyList() + val ordinal = localTypes.asSequence().take(index).filter { it == localType }.count() + val localName = localType.typeNameToInsert().replace("[]", "Array") + (ordinal + 1) + val isImplicit = localTypes.count { it == localType } == 1 + return listOf( + createUniqueLookup(localName) + .withIcon(PlatformIcons.VARIABLE_ICON) + .withTypeText(localType.presentableName()) + .withLocalDefinition(localName, localType, ordinal, isArgsOnly, isImplicit, mixinClass) + .createEliminable("local $localName", if (isImplicit) -1 else 0) + ) + } + + private fun LookupElementBuilder.withDescTailText(desc: String) = + withTailText( + Type.getArgumentTypes(desc).joinToString(prefix = "(", postfix = ")") { it.presentableName() } + ) + + private fun LookupElement.withTail(tailType: TailType?) = object : TailTypeDecorator(this) { + override fun computeTailType(context: InsertionContext?) = tailType + } + + private fun LookupElementBuilder.withDefinition(id: String, definitionValue: String) = + withDefinition(id, definitionValue) { _, _ -> } + + private fun LookupElementBuilder.withDefinitionAndFold(id: String, foldAttribute: String, definitionValue: String) = + withDefinition(id, definitionValue) { context, annotation -> + val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(context.editor) + CodeFoldingManager.getInstance(context.project).updateFoldRegions(hostEditor) + val foldingModel = hostEditor.foldingModel + val regionsToFold = mutableListOf() + val annotationRange = annotation.textRange + for (foldRegion in foldingModel.allFoldRegions) { + if (!annotationRange.contains(foldRegion.textRange)) { + continue + } + val nameValuePair = annotation.findElementAt(foldRegion.startOffset - annotationRange.startOffset) + ?.findContainingNameValuePair() ?: continue + if (nameValuePair.name == foldAttribute && + nameValuePair.parentOfType()?.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) + == true + ) { + regionsToFold += foldRegion + } + } + + foldingModel.runBatchFoldingOperation { + for (foldRegion in regionsToFold) { + foldRegion.isExpanded = false + } + } + } + + private fun LookupElementBuilder.withLocalDefinition( + name: String, + type: Type, + ordinal: Int, + isArgsOnly: Boolean, + canBeImplicit: Boolean, + mixinClass: PsiClass, + ): LookupElementBuilder { + val isTypeAccessible = type.isAccessibleFrom(mixinClass) + val isImplicit = canBeImplicit && isTypeAccessible + + val definitionValue = buildString { + append("local = @${MixinConstants.MixinExtras.LOCAL}(") + if (isTypeAccessible) { + append("type = ${type.className}.class, ") + } + if (!isImplicit) { + append("ordinal = ") + append(ordinal) + append(", ") + } + if (isArgsOnly) { + append("argsOnly = true, ") + } + + if (endsWith(", ")) { + setLength(length - 2) + } + + append(")") + } + return withDefinition(name, definitionValue) { context, annotation -> + if (isImplicit) { + return@withDefinition + } + + invokeLater { + WriteCommandAction.runWriteCommandAction( + context.project, + "Choose How to Target Local Variable", + null, + { runLocalTemplate(context.project, context.editor, context.file, annotation, ordinal, name) }, + annotation.containingFile, + ) + } + } + } + + private fun runLocalTemplate( + project: Project, + editor: Editor, + file: PsiFile, + annotation: PsiAnnotation, + ordinal: Int, + name: String + ) { + val elementToReplace = + (annotation.findDeclaredAttributeValue("local") as? PsiAnnotation) + ?.findDeclaredAttributeValue("ordinal") + ?.findContainingNameValuePair() ?: return + + val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(editor) + val hostElement = file.findElementAt(editor.caretModel.offset)?.findMultiInjectionHost() ?: return + + val template = TemplateBuilderImpl(annotation) + val lookupItems = arrayOf( + LookupElementBuilder.create("ordinal = $ordinal"), + LookupElementBuilder.create("name = \"$name\"") + ) + template.replaceElement( + elementToReplace, + object : Expression() { + override fun calculateLookupItems(context: TemplateExpressionContext?) = lookupItems + override fun calculateQuickResult(context: TemplateExpressionContext?) = calculateResult(context) + override fun calculateResult(context: TemplateExpressionContext?) = + TextResult("ordinal = $ordinal") + }, + true, + ) + + val prevCursorPosInLiteral = hostEditor.caretModel.offset - hostElement.textRange.startOffset + val hostElementPtr = hostElement.createSmartPointer(project) + hostEditor.caretModel.moveToOffset(annotation.textRange.startOffset) + TemplateManager.getInstance(project).startTemplate( + hostEditor, + template.buildInlineTemplate(), + object : TemplateEditingAdapter() { + override fun templateFinished(template: Template, brokenOff: Boolean) { + PsiDocumentManager.getInstance(project).commitDocument(hostEditor.document) + val newHostElement = hostElementPtr.element ?: return + hostEditor.caretModel.moveToOffset(newHostElement.textRange.startOffset + prevCursorPosInLiteral) + } + } + ) + } + + private inline fun LookupElementBuilder.withDefinition( + id: String, + definitionValue: String, + crossinline andThen: (InsertionContext, PsiAnnotation) -> Unit + ) = withInsertHandler { context, _ -> + context.laterRunnable = Runnable { + context.commitDocument() + CommandProcessor.getInstance().runUndoTransparentAction { + runWriteAction { + val annotation = addDefinition(context, id, definitionValue) + if (annotation != null) { + andThen(context, annotation) + } + } + } + } + } + + private fun addDefinition(context: InsertionContext, id: String, definitionValue: String): PsiAnnotation? { + val contextElement = context.file.findElementAt(context.startOffset) ?: return null + return addDefinition(context.project, contextElement, id, definitionValue) + } + + fun addDefinition( + project: Project, + contextElement: PsiElement, + id: String, + definitionValue: String + ): PsiAnnotation? { + val injectionHost = contextElement.findMultiInjectionHost() ?: return null + val expressionAnnotation = injectionHost.parentOfType() ?: return null + if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + return null + } + val modifierList = expressionAnnotation.findContainingModifierList() ?: return null + + // look for an existing definition with this id, skip if it exists + for (annotation in modifierList.annotations) { + if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) && + annotation.findDeclaredAttributeValue("id")?.constantStringValue == id + ) { + return null + } + } + + // create and add the new @Definition annotation + var newAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText( + "@${MixinConstants.MixinExtras.DEFINITION}(id = \"$id\", $definitionValue)", + modifierList, + ) + var anchor = modifierList.annotations.lastOrNull { it.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) } + if (anchor == null) { + val definitionPosRelativeToExpression = + MinecraftProjectSettings.getInstance(project).definitionPosRelativeToExpression + if (definitionPosRelativeToExpression == BeforeOrAfter.AFTER) { + anchor = expressionAnnotation + } + } + newAnnotation = modifierList.addAfter(newAnnotation, anchor) as PsiAnnotation + + // add imports and reformat + newAnnotation = + JavaCodeStyleManager.getInstance(project).shortenClassReferences(newAnnotation) as PsiAnnotation + JavaCodeStyleManager.getInstance(project).optimizeImports(modifierList.containingFile) + val annotationIndex = modifierList.annotations.indexOf(newAnnotation) + val formattedModifierList = + CodeStyleManager.getInstance(project).reformat(modifierList) as PsiModifierList + return formattedModifierList.annotations.getOrNull(annotationIndex) + } + + private fun getStatementVariants( + factory: MEExpressionElementFactory, + statement: MEStatement + ): List { + return if (statement is MEExpressionStatement) { + getExpressionVariants(factory, statement.expression) + } else { + listOf(statement) + } + } + + private fun getExpressionVariants( + factory: MEExpressionElementFactory, + expression: MEExpression + ): List { + val variants = mutableListOf(expression) + + val assignmentStatement = factory.createStatement("? = ?") as MEAssignStatement + assignmentStatement.targetExpr.replace(expression.copy()) + variants += assignmentStatement + + when (expression) { + is MEParenthesizedExpression -> { + val castExpr = factory.createExpression("(?) ?") as MECastExpression + castExpr.castTypeExpr!!.replace(expression.copy()) + variants += castExpr + } + is MENameExpression -> { + val callExpr = factory.createExpression("?()") as MEStaticMethodCallExpression + callExpr.memberName.replace(expression.meName) + variants += callExpr + + val classExpr = factory.createExpression("${expression.text}.class") as MEClassConstantExpression + variants += classExpr + } + is MEMemberAccessExpression -> { + val callExpr = factory.createExpression("?.?()") as MEMethodCallExpression + callExpr.receiverExpr.replace(expression.receiverExpr) + callExpr.memberName.replace(expression.memberName) + variants += callExpr + } + is MENewExpression -> { + val type = expression.type + if (type != null && !expression.hasConstructorArguments && !expression.isArrayCreation) { + val fixedNewExpr = factory.createExpression("new ?()") as MENewExpression + fixedNewExpr.type!!.replace(type) + variants += fixedNewExpr + + val fixedNewArrayExpr = factory.createExpression("new ?[?]") as MENewExpression + fixedNewArrayExpr.type!!.replace(type) + variants += fixedNewArrayExpr + + val arrayLitExpr = factory.createExpression("new ?[]{?}") as MENewExpression + arrayLitExpr.type!!.replace(type) + variants += arrayLitExpr + } + } + is MESuperCallExpression -> { + // Might be missing its parentheses + val callExpr = factory.createExpression("super.?()") as MESuperCallExpression + expression.memberName?.let { callExpr.memberName!!.replace(it) } + variants += callExpr + } + } + + return variants + } + + private fun createUniqueLookup(text: String) = LookupElementBuilder.create(Any(), text) + + private fun LookupElement.createEliminable(uniquenessKey: String, priority: Int = 0) = + EliminableLookup(uniquenessKey, this, priority) + + private class EliminableLookup( + val uniquenessKey: String, + val lookupElement: LookupElement, + private val priority: Int + ) : Comparable { + override fun compareTo(other: EliminableLookup) = priority.compareTo(other.priority) + } + + private data class ExpandedInstruction(val insn: VirtualInsn, val originalInsn: AbstractInsnNode) + + private class ParenthesesTailType(private val hasParameters: Boolean) : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, "()") + return moveCaret(editor, tailOffset, if (hasParameters) 1 else 2) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val offset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + return !CharArrayUtil.regionMatches(chars, offset, "(") + } + } + + private class BracketsTailType(private val dimensions: Int, private val hasInitializer: Boolean) : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, "[]".repeat(dimensions) + if (hasInitializer) "{}" else "") + return moveCaret(editor, tailOffset, if (hasInitializer) 2 * dimensions + 1 else 1) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val offset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + return !CharArrayUtil.regionMatches(chars, offset, "[") + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt new file mode 100644 index 000000000..1f93df64b --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt @@ -0,0 +1,73 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEClassConstantExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFileFactory +import com.intellij.util.IncorrectOperationException + +class MEExpressionElementFactory(private val project: Project) { + fun createFile(text: String): MEExpressionFile { + return PsiFileFactory.getInstance(project).createFileFromText( + "dummy.mixinextrasexpression", + MEExpressionFileType, + text + ) as MEExpressionFile + } + + fun createStatement(text: String): MEStatement { + return createFile("do {$text}").statements.singleOrNull() + ?: throw IncorrectOperationException("'$text' is not a statement") + } + + fun createExpression(text: String): MEExpression { + return (createStatement(text) as? MEExpressionStatement)?.expression + ?: throw IncorrectOperationException("'$text' is not an expression") + } + + fun createName(text: String): MEName { + return (createExpression(text) as? MENameExpression)?.meName + ?: throw IncorrectOperationException("'$text' is not a name") + } + + fun createIdentifier(text: String): PsiElement { + return createName(text).identifierElement + ?: throw IncorrectOperationException("'$text' is not an identifier") + } + + fun createType(text: String): METype { + return (createExpression("$text.class") as? MEClassConstantExpression)?.type + ?: throw IncorrectOperationException("'$text' is not a type") + } + + fun createType(name: MEName) = createType(name.text) +} + +val Project.meExpressionElementFactory get() = MEExpressionElementFactory(this) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt new file mode 100644 index 000000000..87d4bd31c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.asset.PlatformAssets +import com.intellij.openapi.fileTypes.LanguageFileType + +object MEExpressionFileType : LanguageFileType(MEExpressionLanguage) { + override fun getName() = "MixinExtras Expression File" + override fun getDescription() = "MixinExtras expression file" + override fun getDefaultExtension() = "mixinextrasexpression" + override fun getIcon() = PlatformAssets.MIXIN_ICON +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt new file mode 100644 index 000000000..22de47c71 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt @@ -0,0 +1,207 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findContainingNameValuePair +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.lang.injection.MultiHostInjector +import com.intellij.lang.injection.MultiHostRegistrar +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.component1 +import com.intellij.openapi.util.component2 +import com.intellij.psi.ElementManipulators +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLanguageInjectionHost +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiParenthesizedExpression +import com.intellij.psi.PsiPolyadicExpression +import com.intellij.psi.impl.source.tree.injected.JavaConcatenationToInjectorAdapter +import com.intellij.psi.util.PsiLiteralUtil +import com.intellij.psi.util.PsiModificationTracker +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.parentOfType +import com.intellij.util.SmartList + +class MEExpressionInjector : MultiHostInjector { + companion object { + private val ELEMENTS = listOf(PsiLiteralExpression::class.java) + private val ME_EXPRESSION_INJECTION = Key.create("mcdev.meExpressionInjection") + + private val CLASS_INJECTION_RESULT = + Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionResult") + private val CLASS_INJECTION_REGISTRAR_IMPL = + Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionRegistrarImpl") + @JvmStatic + private val METHOD_ADD_TO_RESULTS = + CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("addToResults", CLASS_INJECTION_RESULT) + .also { it.isAccessible = true } + @JvmStatic + private val METHOD_GET_INJECTED_RESULT = + CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("getInjectedResult") + .also { it.isAccessible = true } + } + + private data class MEExpressionInjection(val modCount: Long, val injectionResult: Any) + + private fun shouldInjectIn(anchor: PsiElement): Boolean { + val nameValuePair = anchor.findContainingNameValuePair() ?: return false + return when (nameValuePair.name) { + "value", null -> nameValuePair.parentOfType() + ?.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION) == true + "id" -> nameValuePair.parentOfType() + ?.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) == true + else -> false + } + } + + override fun getLanguagesToInject(registrar: MultiHostRegistrar, context: PsiElement) { + val project = context.project + val (anchor, _) = JavaConcatenationToInjectorAdapter(project).computeAnchorAndOperands(context) + + if (!shouldInjectIn(anchor)) { + return + } + + val modifierList = anchor.findContainingModifierList() ?: return + + val modCount = PsiModificationTracker.getInstance(project).modificationCount + val primaryElement = modifierList.getUserData(ME_EXPRESSION_INJECTION) + if (primaryElement != null && primaryElement.modCount == modCount) { + METHOD_ADD_TO_RESULTS.invoke(registrar, primaryElement.injectionResult) + return + } + + // A Frankenstein injection is an injection where we don't know the entire contents, and therefore errors should + // not be reported. + var isFrankenstein = false + registrar.startInjecting(MEExpressionLanguage) + + for (annotation in modifierList.annotations) { + if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + val idExpr = annotation.findDeclaredAttributeValue("id") ?: continue + val isType = annotation.findDeclaredAttributeValue("type") != null + var needsPrefix = true + iterateConcatenation(idExpr) { op -> + if (op is PsiLanguageInjectionHost) { + for (textRange in getTextRanges(op)) { + val prefix = "\nclass $isType ".takeIf { needsPrefix } + needsPrefix = false + registrar.addPlace(prefix, null, op, textRange) + } + } else { + isFrankenstein = true + } + } + } else if (annotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + val valueExpr = annotation.findDeclaredAttributeValue("value") ?: continue + val places = mutableListOf>() + iterateConcatenation(valueExpr) { op -> + if (op is PsiLanguageInjectionHost) { + for (textRange in getTextRanges(op)) { + places += op to textRange + } + } else { + isFrankenstein = true + } + } + if (places.isNotEmpty()) { + for ((i, place) in places.withIndex()) { + val (host, range) = place + val prefix = "\ndo { ".takeIf { i == 0 } + val suffix = " }".takeIf { i == places.size - 1 } + registrar.addPlace(prefix, suffix, host, range) + } + } + } + } + + registrar.doneInjecting() + + modifierList.putUserData( + ME_EXPRESSION_INJECTION, + MEExpressionInjection(modCount, METHOD_GET_INJECTED_RESULT.invoke(registrar)) + ) + + if (isFrankenstein) { + @Suppress("DEPRECATION") // no replacement for this method + com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil.putInjectedFileUserData( + context, + MEExpressionLanguage, + InjectedLanguageManager.FRANKENSTEIN_INJECTION, + true + ) + } + } + + private fun iterateConcatenation(element: PsiElement, consumer: (PsiElement) -> Unit) { + when (element) { + is PsiParenthesizedExpression -> { + val inner = PsiUtil.skipParenthesizedExprDown(element) ?: return + iterateConcatenation(inner, consumer) + } + is PsiPolyadicExpression -> { + if (element.operationTokenType == JavaTokenType.PLUS) { + for (operand in element.operands) { + iterateConcatenation(operand, consumer) + } + } else { + consumer(element) + } + } + else -> consumer(element) + } + } + + private fun getTextRanges(host: PsiLanguageInjectionHost): List { + if (host is PsiLiteralExpression && host.isTextBlock) { + val textRange = ElementManipulators.getValueTextRange(host) + val indent = PsiLiteralUtil.getTextBlockIndent(host) + if (indent <= 0) { + return listOf(textRange) + } + + val text = (host as PsiElement).text + var startOffset = textRange.startOffset + indent + var endOffset = text.indexOf('\n', startOffset) + val result = SmartList() + while (endOffset > 0) { + endOffset++ + result.add(TextRange(startOffset, endOffset)) + startOffset = endOffset + indent + endOffset = text.indexOf('\n', startOffset) + } + endOffset = textRange.endOffset + if (startOffset < endOffset) { + result.add(TextRange(startOffset, endOffset)) + } + return result + } else { + return listOf(ElementManipulators.getValueTextRange(host)) + } + } + + override fun elementsToInjectIn() = ELEMENTS +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt new file mode 100644 index 000000000..af5496462 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt @@ -0,0 +1,25 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.intellij.lang.Language + +object MEExpressionLanguage : Language("MEExpression") diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt new file mode 100644 index 000000000..8eb499188 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt @@ -0,0 +1,25 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.intellij.lexer.FlexAdapter + +class MEExpressionLexerAdapter : FlexAdapter(MEExpressionLexer(null)) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt new file mode 100644 index 000000000..541cdd455 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt @@ -0,0 +1,317 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.cached +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.computeStringArray +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.findAnnotations +import com.demonwav.mcdev.util.resolveType +import com.demonwav.mcdev.util.resolveTypeArray +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiModifierList +import com.llamalad7.mixinextras.expression.impl.ExpressionParserFacade +import com.llamalad7.mixinextras.expression.impl.ExpressionService +import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression +import com.llamalad7.mixinextras.expression.impl.flow.ComplexDataException +import com.llamalad7.mixinextras.expression.impl.flow.FlowInterpreter +import com.llamalad7.mixinextras.expression.impl.flow.FlowValue +import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.pool.IdentifierPool +import com.llamalad7.mixinextras.expression.impl.pool.SimpleMemberDefinition +import org.objectweb.asm.Handle +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.VarInsnNode +import org.objectweb.asm.tree.analysis.Analyzer + +typealias IdentifierPoolFactory = (MethodNode) -> IdentifierPool +typealias FlowMap = Map + +/** + * An instruction that MixinExtras generates (via instruction expansion), as opposed to an instruction in the original + * method. One type of instruction cannot be directly assigned to another, to avoid a method instruction being used when + * a virtual instruction is expected and vice versa. + */ +@JvmInline +value class VirtualInsn(val insn: AbstractInsnNode) + +object MEExpressionMatchUtil { + private val LOGGER = logger() + + init { + ExpressionService.offerInstance(MEExpressionService) + } + + fun getFlowMap(project: Project, classIn: ClassNode, methodIn: MethodNode): FlowMap? { + if (methodIn.instructions == null) { + return null + } + + return methodIn.cached(classIn, project) { classNode, methodNode -> + val interpreter = object : FlowInterpreter(classNode, methodNode, MEFlowContext(project)) { + override fun newValue(type: Type?): FlowValue? { + ProgressManager.checkCanceled() + return super.newValue(type) + } + + override fun newOperation(insn: AbstractInsnNode?): FlowValue? { + ProgressManager.checkCanceled() + return super.newOperation(insn) + } + + override fun copyOperation(insn: AbstractInsnNode?, value: FlowValue?): FlowValue? { + ProgressManager.checkCanceled() + return super.copyOperation(insn, value) + } + + override fun unaryOperation(insn: AbstractInsnNode?, value: FlowValue?): FlowValue? { + ProgressManager.checkCanceled() + return super.unaryOperation(insn, value) + } + + override fun binaryOperation( + insn: AbstractInsnNode?, + value1: FlowValue?, + value2: FlowValue? + ): FlowValue? { + ProgressManager.checkCanceled() + return super.binaryOperation(insn, value1, value2) + } + + override fun ternaryOperation( + insn: AbstractInsnNode?, + value1: FlowValue?, + value2: FlowValue?, + value3: FlowValue? + ): FlowValue? { + ProgressManager.checkCanceled() + return super.ternaryOperation(insn, value1, value2, value3) + } + + override fun naryOperation(insn: AbstractInsnNode?, values: MutableList?): FlowValue? { + ProgressManager.checkCanceled() + return super.naryOperation(insn, values) + } + + override fun returnOperation(insn: AbstractInsnNode?, value: FlowValue?, expected: FlowValue?) { + ProgressManager.checkCanceled() + super.returnOperation(insn, value, expected) + } + + override fun merge(value1: FlowValue?, value2: FlowValue?): FlowValue? { + ProgressManager.checkCanceled() + return super.merge(value1, value2) + } + } + + try { + Analyzer(interpreter).analyze(classNode.name, methodNode) + } catch (e: RuntimeException) { + if (e is ProcessCanceledException) { + throw e + } + LOGGER.warn("MEExpressionMatchUtil.getFlowMap failed", e) + return@cached null + } + + interpreter.finish().asSequence().mapNotNull { flow -> flow.virtualInsnOrNull?.let { it to flow } }.toMap() + } + } + + fun createIdentifierPoolFactory( + module: Module, + targetClass: ClassNode, + modifierList: PsiModifierList, + ): IdentifierPoolFactory = { targetMethod -> + val pool = IdentifierPool() + + for (annotation in modifierList.annotations) { + if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + continue + } + + val definitionId = annotation.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList() + for (field in fields) { + val fieldRef = MemberReference.parse(field) ?: continue + pool.addMember( + definitionId, + SimpleMemberDefinition { + it is FieldInsnNode && fieldRef.matchField(it.owner, it.name, it.desc) + } + ) + } + + val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList() + for (method in methods) { + val methodRef = MemberReference.parse(method) ?: continue + pool.addMember( + definitionId, + object : SimpleMemberDefinition { + override fun matches(insn: AbstractInsnNode) = + insn is MethodInsnNode && methodRef.matchMethod(insn.owner, insn.name, insn.desc) + + override fun matches(handle: Handle) = + handle.tag in Opcodes.H_INVOKEVIRTUAL..Opcodes.H_INVOKEINTERFACE && + methodRef.matchMethod(handle.owner, handle.name, handle.desc) + } + ) + } + + val types = annotation.findDeclaredAttributeValue("type")?.resolveTypeArray() ?: emptyList() + for (type in types) { + val asmType = Type.getType(type.descriptor) + pool.addType(definitionId) { it == asmType } + } + + val locals = annotation.findDeclaredAttributeValue("local")?.findAnnotations() ?: emptyList() + for (localAnnotation in locals) { + val localType = localAnnotation.findDeclaredAttributeValue("type")?.resolveType() + val localInfo = LocalInfo.fromAnnotation(localType, localAnnotation) + pool.addMember(definitionId) { node -> + val virtualInsn = node.insn + if (virtualInsn !is VarInsnNode) { + return@addMember false + } + val physicalInsn = InsnExpander.getRepresentative(node) + val actualInsn = if (virtualInsn.opcode >= Opcodes.ISTORE && virtualInsn.opcode <= Opcodes.ASTORE) { + physicalInsn.next ?: return@addMember false + } else { + physicalInsn + } + + val unfilteredLocals = localInfo.getLocals(module, targetClass, targetMethod, actualInsn) + ?: return@addMember false + val filteredLocals = localInfo.matchLocals(unfilteredLocals, CollectVisitor.Mode.MATCH_ALL) + filteredLocals.any { it.index == virtualInsn.`var` } + } + } + } + + pool + } + + fun createExpression(text: String): Expression? { + return try { + ExpressionParserFacade.parse(text) + } catch (e: Exception) { + null + } catch (e: StackOverflowError) { + null + } + } + + fun getContextType(project: Project, annotationName: String?): ExpressionContext.Type { + if (annotationName == null) { + return ExpressionContext.Type.CUSTOM + } + if (annotationName == MixinConstants.Annotations.SLICE) { + return ExpressionContext.Type.SLICE + } + + val handler = MixinAnnotationHandler.forMixinAnnotation(annotationName, project) as? InjectorAnnotationHandler + ?: return ExpressionContext.Type.CUSTOM + return handler.mixinExtrasExpressionContextType + } + + inline fun findMatchingInstructions( + targetClass: ClassNode, + targetMethod: MethodNode, + pool: IdentifierPool, + flows: FlowMap, + expr: Expression, + insns: Iterable, + contextType: ExpressionContext.Type, + forCompletion: Boolean, + callback: (ExpressionMatch) -> Unit + ) { + for (insn in insns) { + val decorations = mutableMapOf>() + val captured = mutableListOf>() + + val sink = object : Expression.OutputSink { + override fun capture(node: FlowValue, expr: Expression?, ctx: ExpressionContext?) { + captured += node to (expr?.src?.startIndex ?: 0) + decorations.getOrPut(insn, ::mutableMapOf).putAll(node.decorations) + } + + override fun decorate(insn: AbstractInsnNode, key: String, value: Any?) { + decorations.getOrPut(VirtualInsn(insn), ::mutableMapOf)[key] = value + } + + override fun decorateInjectorSpecific(insn: AbstractInsnNode, key: String, value: Any?) { + // Our maps are per-injector anyway, so this is just a normal decoration. + decorations.getOrPut(VirtualInsn(insn), ::mutableMapOf)[key] = value + } + } + + val flow = flows[insn] ?: continue + try { + val context = ExpressionContext(pool, sink, targetClass, targetMethod, contextType, forCompletion) + if (expr.matches(flow, context)) { + for ((capturedFlow, startOffset) in captured) { + val capturedInsn = capturedFlow.virtualInsnOrNull ?: continue + val originalInsn = InsnExpander.getRepresentative(capturedFlow) ?: capturedInsn.insn + callback(ExpressionMatch(flow, originalInsn, startOffset, decorations[capturedInsn].orEmpty())) + } + } + } catch (e: ProcessCanceledException) { + throw e + } catch (ignored: Exception) { + // MixinExtras throws lots of different exceptions + } + } + } + + val FlowValue.virtualInsn: VirtualInsn get() = VirtualInsn(insn) + + val FlowValue.virtualInsnOrNull: VirtualInsn? get() = try { + VirtualInsn(insn) + } catch (e: ComplexDataException) { + null + } + + class ExpressionMatch @PublishedApi internal constructor( + val flow: FlowValue, + val originalInsn: AbstractInsnNode, + val startOffset: Int, + val decorations: Map, + ) +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt new file mode 100644 index 000000000..7d16e8816 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt @@ -0,0 +1,46 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.MEExpressionParser +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets +import com.intellij.lang.ASTNode +import com.intellij.lang.ParserDefinition +import com.intellij.openapi.project.Project +import com.intellij.psi.FileViewProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.tree.IFileElementType +import com.intellij.psi.tree.TokenSet + +class MEExpressionParserDefinition : ParserDefinition { + + override fun createLexer(project: Project) = MEExpressionLexerAdapter() + override fun getCommentTokens(): TokenSet = TokenSet.EMPTY + override fun getStringLiteralElements() = MEExpressionTokenSets.STRINGS + override fun createParser(project: Project) = MEExpressionParser() + override fun getFileNodeType() = FILE + override fun createFile(viewProvider: FileViewProvider) = MEExpressionFile(viewProvider) + override fun createElement(node: ASTNode): PsiElement = MEExpressionTypes.Factory.createElement(node) +} + +val FILE = IFileElementType(MEExpressionLanguage) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt new file mode 100644 index 000000000..dc1a1796d --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt @@ -0,0 +1,26 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets +import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler + +class MEExpressionQuoteHandler : SimpleTokenSetQuoteHandler(MEExpressionTokenSets.STRINGS) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt new file mode 100644 index 000000000..2294b4bdd --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt @@ -0,0 +1,29 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.intellij.lang.refactoring.RefactoringSupportProvider +import com.intellij.psi.PsiElement + +class MEExpressionRefactoringSupport : RefactoringSupportProvider() { + // Inplace renaming doesn't work due to IDEA-348784 + override fun isInplaceRenameAvailable(element: PsiElement, context: PsiElement?) = false +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt new file mode 100644 index 000000000..473717217 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt @@ -0,0 +1,76 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.util.toPsiType +import com.demonwav.mcdev.util.descriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElementFactory +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiType +import com.llamalad7.mixinextras.expression.impl.ExpressionService +import com.llamalad7.mixinextras.expression.impl.flow.FlowContext +import org.objectweb.asm.Type + +object MEExpressionService : ExpressionService() { + override fun getCommonSuperClass(ctx: FlowContext, type1: Type, type2: Type): Type { + ctx as MEFlowContext + val elementFactory = JavaPsiFacade.getElementFactory(ctx.project) + return Type.getType( + getCommonSuperClass( + ctx.project, + type1.toPsiType(elementFactory) as PsiClassType, + type2.toPsiType(elementFactory) as PsiClassType + )?.descriptor ?: error("Could not intersect types $type1 and $type2!") + ) + } + + // Copied from ClassInfo + private fun getCommonSuperClass( + project: Project, + type1: PsiType, + type2: PsiType + ): PsiClassType? { + val left = (type1 as? PsiClassType)?.resolve() ?: return null + val right = (type2 as? PsiClassType)?.resolve() ?: return null + + fun objectType() = PsiType.getJavaLangObject(PsiManager.getInstance(project), left.resolveScope) + fun PsiClass.type() = PsiElementFactory.getInstance(project).createType(this) + + if (left.isInheritor(right, true)) { + return right.type() + } + if (right.isInheritor(left, true)) { + return left.type() + } + if (left.isInterface || right.isInterface) { + return objectType() + } + + return generateSequence(left) { it.superClass } + .firstOrNull { right.isInheritor(it, true) } + ?.type() + ?: objectType() + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt new file mode 100644 index 000000000..a44fdc28c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt @@ -0,0 +1,198 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.HighlighterColors +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey +import com.intellij.openapi.fileTypes.SyntaxHighlighterBase +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.TokenType +import com.intellij.psi.tree.IElementType + +class MEExpressionSyntaxHighlighter : SyntaxHighlighterBase() { + companion object { + val STRING = createTextAttributesKey( + "MEEXPRESSION_STRING", + DefaultLanguageHighlighterColors.STRING + ) + val STRING_ESCAPE = createTextAttributesKey( + "MEEXPRESSION_STRING_ESCAPE", + DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE + ) + val NUMBER = createTextAttributesKey( + "MEEXPRESSION_NUMBER", + DefaultLanguageHighlighterColors.NUMBER + ) + val KEYWORD = createTextAttributesKey( + "MEEXPRESSION_KEYWORD", + DefaultLanguageHighlighterColors.KEYWORD, + ) + val OPERATOR = createTextAttributesKey( + "MEEXPRESSION_OPERATOR", + DefaultLanguageHighlighterColors.OPERATION_SIGN + ) + val PARENS = createTextAttributesKey( + "MEEXPRESSION_PARENS", + DefaultLanguageHighlighterColors.PARENTHESES + ) + val BRACKETS = createTextAttributesKey( + "MEEXPRESSION_BRACKETS", + DefaultLanguageHighlighterColors.BRACKETS + ) + val BRACES = createTextAttributesKey( + "MEEXPRESSION_BRACES", + DefaultLanguageHighlighterColors.BRACES + ) + val DOT = createTextAttributesKey( + "MEEXPRESSION_DOT", + DefaultLanguageHighlighterColors.DOT + ) + val METHOD_REFERENCE = createTextAttributesKey( + "MEEXPRESSION_METHOD_REFERENCE", + DefaultLanguageHighlighterColors.DOT + ) + val COMMA = createTextAttributesKey( + "MEEXPRESSION_COMMA", + DefaultLanguageHighlighterColors.COMMA + ) + val CAPTURE = createTextAttributesKey( + "MEEXPRESSION_CAPTURE", + DefaultLanguageHighlighterColors.OPERATION_SIGN + ) + val WILDCARD = createTextAttributesKey( + "MEEXPRESSION_WILDCARD", + DefaultLanguageHighlighterColors.OPERATION_SIGN + ) + val IDENTIFIER = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER", + DefaultLanguageHighlighterColors.IDENTIFIER + ) + val IDENTIFIER_CALL = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_CALL", + DefaultLanguageHighlighterColors.FUNCTION_CALL + ) + val IDENTIFIER_CLASS_NAME = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_CLASS_NAME", + DefaultLanguageHighlighterColors.CLASS_REFERENCE + ) + val IDENTIFIER_PRIMITIVE_TYPE = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_PRIMITIVE_TYPE", + DefaultLanguageHighlighterColors.KEYWORD + ) + val IDENTIFIER_MEMBER_NAME = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_MEMBER_NAME", + DefaultLanguageHighlighterColors.INSTANCE_FIELD + ) + val IDENTIFIER_VARIABLE = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_VARIABLE", + DefaultLanguageHighlighterColors.LOCAL_VARIABLE + ) + val IDENTIFIER_TYPE_DECLARATION = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_TYPE_DECLARATION", + DefaultLanguageHighlighterColors.CLASS_NAME + ) + val IDENTIFIER_DECLARATION = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_DECLARATION", + DefaultLanguageHighlighterColors.FUNCTION_DECLARATION + ) + val BAD_CHAR = createTextAttributesKey( + "MEEXPRESSION_BAD_CHARACTER", + HighlighterColors.BAD_CHARACTER + ) + + val STRING_KEYS = arrayOf(STRING) + val STRING_ESCAPE_KEYS = arrayOf(STRING_ESCAPE) + val NUMBER_KEYS = arrayOf(NUMBER) + val KEYWORD_KEYS = arrayOf(KEYWORD) + val OPERATOR_KEYS = arrayOf(OPERATOR) + val PARENS_KEYS = arrayOf(PARENS) + val BRACKETS_KEYS = arrayOf(BRACKETS) + val BRACES_KEYS = arrayOf(BRACES) + val DOT_KEYS = arrayOf(DOT) + val METHOD_REFERENCE_KEYS = arrayOf(METHOD_REFERENCE) + val COMMA_KEYS = arrayOf(COMMA) + val CAPTURE_KEYS = arrayOf(CAPTURE) + val WILDCARD_KEYS = arrayOf(WILDCARD) + val IDENTIFIER_KEYS = arrayOf(IDENTIFIER) + val BAD_CHAR_KEYS = arrayOf(BAD_CHAR) + } + + override fun getHighlightingLexer() = MEExpressionLexerAdapter() + override fun getTokenHighlights(tokenType: IElementType): Array { + if (tokenType == MEExpressionTypes.TOKEN_STRING_ESCAPE) { + return STRING_ESCAPE_KEYS + } + if (MEExpressionTokenSets.STRINGS.contains(tokenType)) { + return STRING_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_IDENTIFIER) { + return IDENTIFIER_KEYS + } + if (MEExpressionTokenSets.NUMBERS.contains(tokenType)) { + return NUMBER_KEYS + } + if (MEExpressionTokenSets.KEYWORDS.contains(tokenType)) { + return KEYWORD_KEYS + } + if (MEExpressionTokenSets.OPERATORS.contains(tokenType)) { + return OPERATOR_KEYS + } + if (MEExpressionTokenSets.PARENS.contains(tokenType)) { + return PARENS_KEYS + } + if (MEExpressionTokenSets.BRACKETS.contains(tokenType)) { + return BRACKETS_KEYS + } + if (MEExpressionTokenSets.BRACES.contains(tokenType)) { + return BRACES_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_DOT) { + return DOT_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_METHOD_REF) { + return METHOD_REFERENCE_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_COMMA) { + return COMMA_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_AT) { + return CAPTURE_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_WILDCARD) { + return WILDCARD_KEYS + } + if (tokenType == TokenType.BAD_CHARACTER) { + return BAD_CHAR_KEYS + } + + return TextAttributesKey.EMPTY_ARRAY + } +} + +class MEExpressionSyntaxHighlighterFactory : SyntaxHighlighterFactory() { + override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?) = MEExpressionSyntaxHighlighter() +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt new file mode 100644 index 000000000..6d714b43b --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt @@ -0,0 +1,42 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInsight.editorActions.TypedHandlerDelegate +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.util.elementType + +class MEExpressionTypedHandlerDelegate : TypedHandlerDelegate() { + override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result { + if (charTyped == ':' && file.language == MEExpressionLanguage) { + AutoPopupController.getInstance(project).autoPopupMemberLookup(editor) { + val offset = editor.caretModel.offset + it.findElementAt(offset - 1).elementType == MEExpressionTypes.TOKEN_METHOD_REF + } + return Result.STOP + } + return Result.CONTINUE + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt b/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt new file mode 100644 index 000000000..e7d22f578 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt @@ -0,0 +1,26 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.intellij.openapi.project.Project +import com.llamalad7.mixinextras.expression.impl.flow.FlowContext + +class MEFlowContext(val project: Project) : FlowContext diff --git a/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt new file mode 100644 index 000000000..8a6312d24 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt @@ -0,0 +1,98 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.util.MemberReference +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement + +class MESourceMatchContext(val project: Project) { + @PublishedApi + internal var realElement: PsiElement? = null + private val capturesInternal = mutableListOf() + val captures: List get() = capturesInternal + + private val types = mutableMapOf>() + private val fields = mutableMapOf>() + private val methods = mutableMapOf>() + private val localInfos = mutableMapOf>() + + init { + addType("byte", "B") + addType("char", "C") + addType("double", "D") + addType("float", "F") + addType("int", "I") + addType("long", "J") + addType("short", "S") + } + + fun addCapture(capturedElement: PsiElement) { + val element = realElement ?: capturedElement + capturesInternal += element + } + + fun getTypes(key: String): List = types[key] ?: emptyList() + + fun addType(key: String, desc: String) { + types.getOrPut(key, ::mutableListOf) += desc + } + + fun getFields(key: String): List = fields[key] ?: emptyList() + + fun addField(key: String, field: MemberReference) { + fields.getOrPut(key, ::mutableListOf) += field + } + + fun getMethods(key: String): List = methods[key] ?: emptyList() + + fun addMethod(key: String, method: MemberReference) { + methods.getOrPut(key, ::mutableListOf) += method + } + + fun getLocalInfos(key: String): List = localInfos[key] ?: emptyList() + + fun addLocalInfo(key: String, localInfo: LocalInfo) { + localInfos.getOrPut(key, ::mutableListOf) += localInfo + } + + fun reset() { + capturesInternal.clear() + } + + inline fun fakeElementScope( + isFake: Boolean, + realElement: PsiElement, + action: () -> T + ): T { + if (this.realElement != null || !isFake) { + return action() + } + + this.realElement = realElement + try { + return action() + } finally { + this.realElement = null + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt new file mode 100644 index 000000000..697faee8b --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt @@ -0,0 +1,27 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage +import com.intellij.psi.tree.IElementType +import org.jetbrains.annotations.NonNls + +class MEExpressionElementType(@NonNls debugName: String) : IElementType(debugName, MEExpressionLanguage) diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt new file mode 100644 index 000000000..1f9002488 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt @@ -0,0 +1,41 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionFileType +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatementItem +import com.intellij.extapi.psi.PsiFileBase +import com.intellij.psi.FileViewProvider + +class MEExpressionFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, MEExpressionLanguage) { + override fun getFileType() = MEExpressionFileType + override fun toString() = "MixinExtras Expression File" + override fun getIcon(flags: Int) = PlatformAssets.MIXIN_ICON + + val items: Array get() = findChildrenByClass(MEItem::class.java) + val declarations: List get() = items.filterIsInstance() + val statements: List get() = items.mapNotNull { (it as? MEStatementItem)?.statement } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt new file mode 100644 index 000000000..2b6c41702 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt @@ -0,0 +1,45 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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 . + */ + +@file:JvmName("MEExpressionParserUtil") + +package com.demonwav.mcdev.platform.mixin.expression.psi + +import com.intellij.lang.PsiBuilder +import com.intellij.lang.parser.GeneratedParserUtilBase.* // ktlint-disable no-wildcard-imports + +fun parseToRightBracket( + builder: PsiBuilder, + level: Int, + recoverParser: Parser, + rightBracketParser: Parser +): Boolean { + recursion_guard_(builder, level, "parseToRightBracket") + + // continue over any stuff inside the brackets as error elements. We need to find our precious right bracket. + var marker = enter_section_(builder, level, _NONE_) + exit_section_(builder, level, marker, false, false, recoverParser) + + // consume our right bracket. + marker = enter_section_(builder) + val result = rightBracketParser.parse(builder, level) + exit_section_(builder, marker, null, result) + return result +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt new file mode 100644 index 000000000..cd4a1842e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt @@ -0,0 +1,73 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.intellij.psi.tree.TokenSet + +object MEExpressionTokenSets { + val STRINGS = TokenSet.create( + MEExpressionTypes.TOKEN_STRING, + MEExpressionTypes.TOKEN_STRING_ESCAPE, + MEExpressionTypes.TOKEN_STRING_TERMINATOR, + ) + val NUMBERS = TokenSet.create( + MEExpressionTypes.TOKEN_INT_LIT, + MEExpressionTypes.TOKEN_DEC_LIT, + ) + val KEYWORDS = TokenSet.create( + MEExpressionTypes.TOKEN_BOOL_LIT, + MEExpressionTypes.TOKEN_NULL_LIT, + MEExpressionTypes.TOKEN_DO, + MEExpressionTypes.TOKEN_INSTANCEOF, + MEExpressionTypes.TOKEN_NEW, + MEExpressionTypes.TOKEN_RETURN, + MEExpressionTypes.TOKEN_THROW, + MEExpressionTypes.TOKEN_THIS, + MEExpressionTypes.TOKEN_SUPER, + MEExpressionTypes.TOKEN_CLASS, + MEExpressionTypes.TOKEN_RESERVED, + ) + val OPERATORS = TokenSet.create( + MEExpressionTypes.TOKEN_BITWISE_NOT, + MEExpressionTypes.TOKEN_MULT, + MEExpressionTypes.TOKEN_DIV, + MEExpressionTypes.TOKEN_MOD, + MEExpressionTypes.TOKEN_PLUS, + MEExpressionTypes.TOKEN_MINUS, + MEExpressionTypes.TOKEN_SHL, + MEExpressionTypes.TOKEN_SHR, + MEExpressionTypes.TOKEN_USHR, + MEExpressionTypes.TOKEN_LT, + MEExpressionTypes.TOKEN_LE, + MEExpressionTypes.TOKEN_GT, + MEExpressionTypes.TOKEN_GE, + MEExpressionTypes.TOKEN_EQ, + MEExpressionTypes.TOKEN_NE, + MEExpressionTypes.TOKEN_BITWISE_AND, + MEExpressionTypes.TOKEN_BITWISE_XOR, + MEExpressionTypes.TOKEN_BITWISE_OR, + MEExpressionTypes.TOKEN_ASSIGN, + ) + val PARENS = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_PAREN, MEExpressionTypes.TOKEN_RIGHT_PAREN) + val BRACKETS = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_BRACKET, MEExpressionTypes.TOKEN_RIGHT_BRACKET) + val BRACES = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_BRACE, MEExpressionTypes.TOKEN_RIGHT_BRACE) +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt new file mode 100644 index 000000000..54a746b88 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt @@ -0,0 +1,29 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage +import com.intellij.psi.tree.IElementType +import org.jetbrains.annotations.NonNls + +class MEExpressionTokenType(@NonNls debugName: String) : IElementType(debugName, MEExpressionLanguage) { + override fun toString() = "MEExpressionTokenType.${super.toString()}" +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt b/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt new file mode 100644 index 000000000..3474a3067 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.PsiElement + +interface MEMatchableElement : PsiElement { + fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean + + fun getInputExprs(): List +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt b/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt new file mode 100644 index 000000000..ba971b0e2 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt @@ -0,0 +1,34 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.intellij.openapi.util.TextRange +import com.intellij.psi.AbstractElementManipulator + +class MENameElementManipulator : AbstractElementManipulator() { + override fun handleContentChange(element: MEName, range: TextRange, newContent: String): MEName { + val text = element.text + val newText = text.substring(0, range.startOffset) + newContent + text.substring(range.endOffset) + return element.project.meExpressionElementFactory.createName(newText) + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt new file mode 100644 index 000000000..b9856a13e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt @@ -0,0 +1,58 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEAssignStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression + +object MEPsiUtil { + fun isAccessedForReading(expr: MEExpression): Boolean { + return !isAccessedForWriting(expr) + } + + fun isAccessedForWriting(expr: MEExpression): Boolean { + val parent = expr.parent + return parent is MEAssignStatement && expr == parent.targetExpr + } + + fun skipParenthesizedExprDown(expr: MEExpression): MEExpression? { + var e: MEExpression? = expr + while (e is MEParenthesizedExpression) { + e = e.expression + } + return e + } + + fun isWildcardExpression(expr: MEExpression): Boolean { + val actualExpr = skipParenthesizedExprDown(expr) ?: return false + return actualExpr is MENameExpression && actualExpr.meName.isWildcard + } + + fun isIdentifierStart(char: Char): Boolean { + return char in 'a'..'z' || char in 'A'..'Z' || char == '_' + } + + fun isIdentifierPart(char: Char): Boolean { + return isIdentifierStart(char) || char in '0'..'9' + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt b/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt new file mode 100644 index 000000000..40f6beab5 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt @@ -0,0 +1,45 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEVisitor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiRecursiveVisitor +import com.intellij.psi.PsiWalkingState + +abstract class MERecursiveWalkingVisitor : MEVisitor(), PsiRecursiveVisitor { + private val walkingState = object : PsiWalkingState(this) { + override fun elementFinished(element: PsiElement) { + this@MERecursiveWalkingVisitor.elementFinished(element) + } + } + + override fun visitElement(element: PsiElement) { + walkingState.elementStarted(element) + } + + open fun elementFinished(element: PsiElement) { + } + + fun stopWalking() { + walkingState.stopWalking() + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt new file mode 100644 index 000000000..95c047703 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt @@ -0,0 +1,125 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBinaryExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECastExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.intellij.patterns.ObjectPattern +import com.intellij.patterns.PatternCondition +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.intellij.util.ProcessingContext + +object METypeUtil { + fun convertExpressionToType(expr: MEExpression): METype? { + return if (isExpressionValidType(expr)) { + expr.project.meExpressionElementFactory.createType(expr.text) + } else { + null + } + } + + private fun isExpressionValidType(expr: MEExpression): Boolean { + var e = expr + while (true) { + when (e) { + is MEArrayAccessExpression -> { + if (e.indexExpr != null || e.rightBracketToken == null) { + return false + } + e = e.arrayExpr + } + is MENameExpression -> return true + else -> return false + } + } + } + + fun isExpressionDirectlyInTypePosition(expr: MEExpression): Boolean { + var e: PsiElement? = expr + while (e != null) { + val parent = e.parent + when (parent) { + is MEArrayAccessExpression -> {} + is MEParenthesizedExpression -> { + val grandparent = parent.parent + return grandparent is MECastExpression && e == grandparent.castTypeExpr + } + is MEBinaryExpression -> { + return parent.operator == MEExpressionTypes.TOKEN_INSTANCEOF && e == parent.rightExpr + } + else -> return false + } + e = parent + } + + return false + } + + fun isExpressionInTypePosition(expr: MEExpression): Boolean { + var e: PsiElement? = expr + while (e != null) { + val parent = e.parent + when (parent) { + is MEParenthesizedExpression -> { + val grandparent = parent.parent + if (grandparent is MECastExpression && e == grandparent.castTypeExpr) { + return true + } + } + is MEBinaryExpression -> { + if (parent.operator == MEExpressionTypes.TOKEN_INSTANCEOF && e == parent.rightExpr) { + return true + } + } + is MEStatement -> return false + } + e = parent + } + + return false + } + + fun > ObjectPattern.inTypePosition(): Self = + with(InTypePositionCondition) + fun > ObjectPattern.notInTypePosition(): Self = + without(InTypePositionCondition) + fun > ObjectPattern.validType(): Self = + with(ValidTypeCondition) + + private object InTypePositionCondition : PatternCondition("inTypePosition") { + override fun accepts(t: PsiElement, context: ProcessingContext?) = + t.parentOfType()?.let(::isExpressionInTypePosition) == true + } + + private object ValidTypeCondition : PatternCondition("validType") { + override fun accepts(t: PsiElement, context: ProcessingContext?) = + t.parentOfType(withSelf = true)?.let(::isExpressionValidType) == true + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt new file mode 100644 index 000000000..2ef6ed9f3 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt @@ -0,0 +1,34 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiExpressionList + +interface MEArgumentsMixin : PsiElement { + fun matchesJava(java: PsiExpressionList, context: MESourceMatchContext): Boolean { + return matchesJava(java.expressions, context) + } + + fun matchesJava(java: Array, context: MESourceMatchContext): Boolean +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt new file mode 100644 index 000000000..c026499db --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt @@ -0,0 +1,29 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.PsiElement + +interface MEArrayAccessExpressionMixin : MEExpression { + val leftBracketToken: PsiElement + val rightBracketToken: PsiElement? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt new file mode 100644 index 000000000..1631852dd --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.intellij.psi.tree.IElementType + +interface MEBinaryExpressionMixin : MEExpression { + val operator: IElementType + val castType: METype? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt new file mode 100644 index 000000000..3e61e8c13 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype + +interface MECastExpressionMixin : MEExpression { + val castType: METype? + val castTypeExpr: MEExpression? + val castedExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt new file mode 100644 index 000000000..cb8d52136 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt @@ -0,0 +1,27 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.intellij.psi.PsiElement + +interface MEDeclarationItemMixin : PsiElement { + val isType: Boolean +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt new file mode 100644 index 000000000..e1a376343 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.lang.ASTNode + +interface MELitExpressionMixin : MEExpression { + val value: Any? + val isNull: Boolean + val isString: Boolean + val minusToken: ASTNode? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt new file mode 100644 index 000000000..5e344d9d2 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt @@ -0,0 +1,28 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.intellij.psi.PsiElement + +interface MENameMixin : PsiElement { + val isWildcard: Boolean + val identifierElement: PsiElement? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt new file mode 100644 index 000000000..31b29b4a9 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt @@ -0,0 +1,35 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.PsiElement + +interface MENewExpressionMixin : PsiElement { + val isArrayCreation: Boolean + val hasConstructorArguments: Boolean + val dimensions: Int + val dimExprTokens: List + val arrayInitializer: MEArguments? + + class DimExprTokens(val leftBracket: PsiElement, val expr: MEExpression?, val rightBracket: PsiElement?) +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt new file mode 100644 index 000000000..30d404f6c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiType + +interface METypeMixin : PsiElement { + val isArray: Boolean + val dimensions: Int + + fun matchesJava(java: PsiType, context: MESourceMatchContext): Boolean +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt new file mode 100644 index 000000000..21cb6abf6 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt @@ -0,0 +1,28 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.tree.IElementType + +interface MEUnaryExpressionMixin : MEExpression { + val operator: IElementType +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt new file mode 100644 index 000000000..a83224344 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt @@ -0,0 +1,44 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArgumentsMixin +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiExpression +import com.intellij.psi.util.PsiUtil + +abstract class MEArgumentsImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEArgumentsMixin { + override fun matchesJava(java: Array, context: MESourceMatchContext): Boolean { + val exprs = expressionList + if (exprs.size != java.size) { + return false + } + return exprs.asSequence().zip(java.asSequence()).all { (expr, javaExpr) -> + val actualJavaExpr = PsiUtil.skipParenthesizedExprDown(javaExpr) ?: return@all false + expr.matchesJava(actualJavaExpr, context) + } + } + + protected abstract val expressionList: List +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt new file mode 100644 index 000000000..dcc6f6f9f --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt @@ -0,0 +1,58 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArrayAccessExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiArrayAccessExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiUtil + +abstract class MEArrayAccessExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEArrayAccessExpressionMixin { + override val leftBracketToken get() = findNotNullChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) + override val rightBracketToken get() = findChildByType(MEExpressionTypes.TOKEN_RIGHT_BRACKET) + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiArrayAccessExpression) { + return false + } + + val readMatch = MEPsiUtil.isAccessedForReading(this) && PsiUtil.isAccessedForReading(java) + val writeMatch = MEPsiUtil.isAccessedForWriting(this) && PsiUtil.isAccessedForWriting(java) + if (!readMatch && !writeMatch) { + return false + } + + val javaArray = PsiUtil.skipParenthesizedExprDown(java.arrayExpression) ?: return false + val javaIndex = PsiUtil.skipParenthesizedExprDown(java.indexExpression) ?: return false + return arrayExpr.matchesJava(javaArray, context) && indexExpr?.matchesJava(javaIndex, context) == true + } + + override fun getInputExprs() = listOfNotNull(arrayExpr, indexExpr) + + protected abstract val arrayExpr: MEExpression + protected abstract val indexExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt new file mode 100644 index 000000000..f3fbfb396 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt @@ -0,0 +1,57 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiAssignmentExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.PsiReplacementUtil + +abstract class MEAssignStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiAssignmentExpression) { + return false + } + val isOperatorAssignment = java.operationTokenType != JavaTokenType.EQ + val expandedJava = if (isOperatorAssignment) { + PsiReplacementUtil.replaceOperatorAssignmentWithAssignmentExpression(java.copy() as PsiAssignmentExpression) + as PsiAssignmentExpression + } else { + java + } + + val leftJava = PsiUtil.skipParenthesizedExprDown(expandedJava.lExpression) ?: return false + val rightJava = PsiUtil.skipParenthesizedExprDown(expandedJava.rExpression) ?: return false + context.fakeElementScope(isOperatorAssignment, java) { + return targetExpr.matchesJava(leftJava, context) && rightExpr?.matchesJava(rightJava, context) == true + } + } + + override fun getInputExprs() = targetExpr.getInputExprs() + listOfNotNull(rightExpr) + + protected abstract val targetExpr: MEExpression + protected abstract val rightExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt new file mode 100644 index 000000000..96c6ae245 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt @@ -0,0 +1,124 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEBinaryExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiBinaryExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiInstanceOfExpression +import com.intellij.psi.PsiTypeTestPattern +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.JavaPsiPatternUtil +import com.intellij.psi.util.PsiUtil + +abstract class MEBinaryExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEBinaryExpressionMixin { + override val operator get() = node.findChildByType(operatorTokens)!!.elementType + override val castType get() = rightExpr + ?.takeIf { operator == MEExpressionTypes.TOKEN_INSTANCEOF } + ?.let(METypeUtil::convertExpressionToType) + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (operator == MEExpressionTypes.TOKEN_INSTANCEOF) { + if (java !is PsiInstanceOfExpression) { + return false + } + if (!leftExpr.matchesJava(java.operand, context)) { + return false + } + val javaType = java.checkType?.type + ?: (JavaPsiPatternUtil.skipParenthesizedPatternDown(java.pattern) as? PsiTypeTestPattern) + ?.checkType?.type + ?: return false + return castType?.matchesJava(javaType, context) == true + } else { + if (java !is PsiBinaryExpression) { + return false + } + + val operatorMatches = when (java.operationTokenType) { + JavaTokenType.ASTERISK -> operator == MEExpressionTypes.TOKEN_MULT + JavaTokenType.DIV -> operator == MEExpressionTypes.TOKEN_DIV + JavaTokenType.PERC -> operator == MEExpressionTypes.TOKEN_MOD + JavaTokenType.PLUS -> operator == MEExpressionTypes.TOKEN_PLUS + JavaTokenType.MINUS -> operator == MEExpressionTypes.TOKEN_MINUS + JavaTokenType.LTLT -> operator == MEExpressionTypes.TOKEN_SHL + JavaTokenType.GTGT -> operator == MEExpressionTypes.TOKEN_SHR + JavaTokenType.GTGTGT -> operator == MEExpressionTypes.TOKEN_USHR + JavaTokenType.LT -> operator == MEExpressionTypes.TOKEN_LT + JavaTokenType.LE -> operator == MEExpressionTypes.TOKEN_LE + JavaTokenType.GT -> operator == MEExpressionTypes.TOKEN_GT + JavaTokenType.GE -> operator == MEExpressionTypes.TOKEN_GE + JavaTokenType.EQEQ -> operator == MEExpressionTypes.TOKEN_EQ + JavaTokenType.NE -> operator == MEExpressionTypes.TOKEN_NE + JavaTokenType.AND -> operator == MEExpressionTypes.TOKEN_BITWISE_AND + JavaTokenType.XOR -> operator == MEExpressionTypes.TOKEN_BITWISE_XOR + JavaTokenType.OR -> operator == MEExpressionTypes.TOKEN_BITWISE_OR + else -> false + } + if (!operatorMatches) { + return false + } + + val javaLeft = PsiUtil.skipParenthesizedExprDown(java.lOperand) ?: return false + val javaRight = PsiUtil.skipParenthesizedExprDown(java.rOperand) ?: return false + return leftExpr.matchesJava(javaLeft, context) && rightExpr?.matchesJava(javaRight, context) == true + } + } + + override fun getInputExprs() = if (operator == MEExpressionTypes.TOKEN_INSTANCEOF) { + listOf(leftExpr) + } else { + listOfNotNull(leftExpr, rightExpr) + } + + protected abstract val leftExpr: MEExpression + protected abstract val rightExpr: MEExpression? + + companion object { + private val operatorTokens = TokenSet.create( + MEExpressionTypes.TOKEN_MULT, + MEExpressionTypes.TOKEN_DIV, + MEExpressionTypes.TOKEN_MOD, + MEExpressionTypes.TOKEN_PLUS, + MEExpressionTypes.TOKEN_MINUS, + MEExpressionTypes.TOKEN_SHL, + MEExpressionTypes.TOKEN_SHR, + MEExpressionTypes.TOKEN_USHR, + MEExpressionTypes.TOKEN_LT, + MEExpressionTypes.TOKEN_LE, + MEExpressionTypes.TOKEN_GT, + MEExpressionTypes.TOKEN_GE, + MEExpressionTypes.TOKEN_EQ, + MEExpressionTypes.TOKEN_NE, + MEExpressionTypes.TOKEN_BITWISE_AND, + MEExpressionTypes.TOKEN_BITWISE_XOR, + MEExpressionTypes.TOKEN_BITWISE_OR, + MEExpressionTypes.TOKEN_INSTANCEOF, + ) + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt new file mode 100644 index 000000000..08e035b61 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt @@ -0,0 +1,64 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodReferenceExpression +import com.intellij.psi.util.PsiUtil + +abstract class MEBoundReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodReferenceExpression) { + return false + } + + if (java.isConstructor) { + return false + } + + val qualifier = PsiUtil.skipParenthesizedExprDown(java.qualifierExpression) ?: return false + if (!receiverExpr.matchesJava(qualifier, context)) { + return false + } + + val memberName = this.memberName ?: return false + if (memberName.isWildcard) { + return true + } + + val method = java.resolve() as? PsiMethod ?: return false + val qualifierClass = QualifiedMember.resolveQualifier(java) ?: method.containingClass ?: return false + return context.getMethods(memberName.text).any { reference -> + reference.matchMethod(method, qualifierClass) + } + } + + override fun getInputExprs() = listOf(receiverExpr) + + abstract val receiverExpr: MEExpression + abstract val memberName: MEName? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt new file mode 100644 index 000000000..3401aab82 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MECapturingExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + context.addCapture(java) + return expression?.matchesJava(java, context) == true + } + + override fun getInputExprs() = listOfNotNull(expression) + + protected abstract val expression: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt new file mode 100644 index 000000000..866999357 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt @@ -0,0 +1,66 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MECastExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiInstanceOfExpression +import com.intellij.psi.PsiTypeCastExpression +import com.intellij.psi.PsiTypeTestPattern +import com.intellij.psi.util.JavaPsiPatternUtil +import com.intellij.psi.util.PsiUtil + +abstract class MECastExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MECastExpressionMixin { + override val castType get() = castTypeExpr?.let(METypeUtil::convertExpressionToType) + override val castTypeExpr get() = + (expressionList.let { it.getOrNull(it.size - 2) } as? MEParenthesizedExpression)?.expression + override val castedExpr get() = expressionList.lastOrNull() + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return when (java) { + is PsiTypeCastExpression -> { + val javaType = java.castType?.type ?: return false + val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false + castType?.matchesJava(javaType, context) == true && + castedExpr?.matchesJava(javaOperand, context) == true + } + is PsiInstanceOfExpression -> { + val pattern = JavaPsiPatternUtil.skipParenthesizedPatternDown(java.pattern) as? PsiTypeTestPattern + ?: return false + val javaType = pattern.checkType?.type ?: return false + val castedExpr = this.castedExpr ?: return false + return MEPsiUtil.isWildcardExpression(castedExpr) && castType?.matchesJava(javaType, context) == true + } + else -> false + } + } + + override fun getInputExprs() = listOfNotNull(castedExpr) + + protected abstract val expressionList: List +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt new file mode 100644 index 000000000..8415f7cd0 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClassObjectAccessExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.util.PsiTypesUtil + +abstract class MEClassConstantExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return when (java) { + is PsiClassObjectAccessExpression -> type.matchesJava(java.operand.type, context) + is PsiReferenceExpression -> { + if (java.referenceName != "TYPE") { + return false + } + val field = java.resolve() as? PsiField ?: return false + val containingClass = field.containingClass?.qualifiedName ?: return false + val unboxedType = PsiTypesUtil.unboxIfPossible(containingClass) + if (unboxedType == null || unboxedType == containingClass) { + return false + } + val javaType = JavaPsiFacade.getElementFactory(context.project).createPrimitiveTypeFromText(unboxedType) + type.matchesJava(javaType, context) + } + else -> false + } + } + + override fun getInputExprs() = emptyList() + + protected abstract val type: METype +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt new file mode 100644 index 000000000..ecaecca53 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt @@ -0,0 +1,47 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodReferenceExpression + +abstract class MEConstructorReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodReferenceExpression) { + return false + } + + if (!java.isConstructor) { + return false + } + + val qualifierType = java.qualifierType?.type ?: return false + return className.matchesJava(qualifierType, context) + } + + override fun getInputExprs() = emptyList() + + abstract val className: METype +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt new file mode 100644 index 000000000..6201fbb9c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt @@ -0,0 +1,63 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEItemImpl +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.intellij.lang.ASTNode +import com.intellij.navigation.ItemPresentation +import com.intellij.openapi.util.Iconable +import com.intellij.psi.NavigatablePsiElement +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNameIdentifierOwner +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.search.LocalSearchScope +import com.intellij.util.PlatformIcons +import javax.swing.Icon + +abstract class MEDeclarationImplMixin( + node: ASTNode +) : MEItemImpl(node), PsiNamedElement, PsiNameIdentifierOwner, NavigatablePsiElement { + override fun getName(): String = nameIdentifier.text + + override fun setName(name: String): PsiElement { + this.nameIdentifier.replace(project.meExpressionElementFactory.createIdentifier(name)) + return this + } + + override fun getNameIdentifier(): PsiElement = firstChild + + override fun getUseScope() = containingFile?.let(::LocalSearchScope) ?: super.getUseScope() + + override fun getPresentation() = object : ItemPresentation { + override fun getPresentableText() = name + + override fun getIcon(unused: Boolean) = this@MEDeclarationImplMixin.getIcon(Iconable.ICON_FLAG_VISIBILITY) + } + + override fun getIcon(flags: Int): Icon = if ((parent as? MEDeclarationItem)?.isType == true) { + PlatformIcons.CLASS_ICON + } else { + PlatformAssets.MIXIN_ICON + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt new file mode 100644 index 000000000..fbd21db66 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEDeclarationItemMixin +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEDeclarationItemImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEDeclarationItemMixin { + override val isType: Boolean + get() = findChildByType(MEExpressionTypes.TOKEN_BOOL_LIT)?.text == "true" +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt new file mode 100644 index 000000000..9cc9b64a6 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEExpressionImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEMatchableElement { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + throw UnsupportedOperationException("Please implement matchesJava for your expression type") + } + + override fun getInputExprs(): List { + throw UnsupportedOperationException("Please implement getInputExprs for your expression type") + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt new file mode 100644 index 000000000..e895d32ba --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt @@ -0,0 +1,37 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEExpressionStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return expression.matchesJava(java, context) + } + + override fun getInputExprs() = listOf(expression) + + protected abstract val expression: MEExpression +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt new file mode 100644 index 000000000..d264eb4ce --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodReferenceExpression + +abstract class MEFreeMethodReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodReferenceExpression) { + return false + } + + if (java.isConstructor) { + return false + } + + val qualifierClass = (java.qualifierType?.type as? PsiClassType)?.resolve() ?: return false + + // check wildcard after checking for the qualifier class, otherwise the reference could have been qualified by + // an expression. + val memberName = this.memberName ?: return false + if (memberName.isWildcard) { + return true + } + + val method = java.resolve() as? PsiMethod ?: return false + return context.getMethods(memberName.text).any { reference -> + reference.matchMethod(method, qualifierClass) + } + } + + override fun getInputExprs() = emptyList() + + abstract val memberName: MEName? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt new file mode 100644 index 000000000..733f689f8 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt @@ -0,0 +1,124 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MELitExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiUnaryExpression +import com.intellij.psi.util.PsiUtil +import com.intellij.util.IncorrectOperationException + +abstract class MELitExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MELitExpressionMixin { + override val value get() = when (node.firstChildNode.elementType) { + MEExpressionTypes.TOKEN_NULL_LIT -> null + MEExpressionTypes.TOKEN_MINUS -> { + when (node.lastChildNode.elementType) { + MEExpressionTypes.TOKEN_INT_LIT -> { + val text = node.lastChildNode.text + if (text.startsWith("0x")) { + "-${text.substring(2)}".toLongOrNull(16) + } else { + "-$text".toLongOrNull() + } + } + MEExpressionTypes.TOKEN_DEC_LIT -> { + "-${node.lastChildNode.text}".toDoubleOrNull() + } + else -> throw IncorrectOperationException("Invalid number literal format") + } + } + MEExpressionTypes.TOKEN_BOOL_LIT -> node.chars[0] == 't' + MEExpressionTypes.TOKEN_INT_LIT -> { + val text = this.text + if (text.startsWith("0x")) { + text.substring(2).toLongOrNull(16) + } else { + text.toLongOrNull() + } + } + MEExpressionTypes.TOKEN_DEC_LIT -> text.toDoubleOrNull() + else -> { + val text = this.text + if (text.length >= 2) { + text.substring(1, text.length - 1).replace("\\'", "'").replace("\\\\", "\\") + } else { + null + } + } + } + + override val isNull get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_NULL_LIT + override val isString get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_STRING_TERMINATOR + + override val minusToken get() = node.firstChildNode.takeIf { it.elementType == MEExpressionTypes.TOKEN_MINUS } + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return when (java) { + is PsiLiteral -> { + val value = this.value + val javaValue = java.value.widened + // MixinExtras compares floats as strings + when (value) { + is Double -> javaValue is Double && value.toString() == javaValue.toString() + is String -> { + val matchesChar = + value.length == 1 && javaValue is Long && value.firstOrNull()?.code?.toLong() == javaValue + matchesChar || value == javaValue + } + else -> value == javaValue + } + } + is PsiUnaryExpression -> { + if (java.operationTokenType != JavaTokenType.MINUS) { + return false + } + val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false + if (javaOperand !is PsiLiteral) { + return false + } + val value = this.value + val javaValue = javaOperand.value.widened + when (value) { + is Long -> javaValue == -value + is Double -> javaValue is Double && javaValue.toString() == (-value).toString() + else -> false + } + } + else -> false + } + } + + override fun getInputExprs() = emptyList() + + private val Any?.widened: Any? get() = when (this) { + is Int -> toLong() + is Float -> toDouble() + is Char -> code.toLong() + else -> this + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt new file mode 100644 index 000000000..4c4c4a11e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt @@ -0,0 +1,71 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.psiutils.ExpressionUtils + +abstract class MEMemberAccessExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiReferenceExpression) { + return false + } + + val arrayFromLength = ExpressionUtils.getArrayFromLengthExpression(java) + if (arrayFromLength != null) { + if (memberName.isWildcard || memberName.text == "length") { + return true + } + } + + val resolved = java.resolve() as? PsiField ?: return false + if (resolved.hasModifierProperty(PsiModifier.STATIC)) { + return false + } + + val javaReceiver = PsiUtil.skipParenthesizedExprDown(java.qualifierExpression) + ?: JavaPsiFacade.getElementFactory(context.project).createExpressionFromText("this", null) + context.fakeElementScope(java.qualifierExpression == null, java) { + if (!receiverExpr.matchesJava(javaReceiver, context)) { + return false + } + } + + val qualifier = QualifiedMember.resolveQualifier(java) ?: resolved.containingClass ?: return false + return context.getFields(memberName.text).any { it.matchField(resolved, qualifier) } + } + + override fun getInputExprs() = listOf(receiverExpr) + + protected abstract val receiverExpr: MEExpression + protected abstract val memberName: MEName +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt new file mode 100644 index 000000000..2f397998e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt @@ -0,0 +1,77 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiModifier +import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.psiutils.MethodCallUtils + +abstract class MEMethodCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodCallExpression) { + return false + } + + if (MethodCallUtils.hasSuperQualifier(java)) { + return false + } + + val method = java.resolveMethod() ?: return false + if (method.hasModifierProperty(PsiModifier.STATIC)) { + return false + } + + if (!memberName.isWildcard) { + val methodId = memberName.text + val qualifier = + QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false + if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) { + return false + } + } + + val javaReceiver = PsiUtil.skipParenthesizedExprDown(java.methodExpression.qualifierExpression) + ?: JavaPsiFacade.getElementFactory(context.project).createExpressionFromText("this", null) + context.fakeElementScope(java.methodExpression.qualifierExpression == null, java.methodExpression) { + if (!receiverExpr.matchesJava(javaReceiver, context)) { + return false + } + } + + return arguments?.matchesJava(java.argumentList, context) == true + } + + override fun getInputExprs() = listOf(receiverExpr) + (arguments?.expressionList ?: emptyList()) + + protected abstract val receiverExpr: MEExpression + protected abstract val memberName: MEName + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt new file mode 100644 index 000000000..decc485d4 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt @@ -0,0 +1,78 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.demonwav.mcdev.platform.mixin.util.LocalVariables +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.PsiVariable +import com.intellij.psi.util.PsiUtil + +abstract class MENameExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (MEName.isWildcard) { + return true + } + + if (java !is PsiReferenceExpression) { + return false + } + val variable = java.resolve() as? PsiVariable ?: return false + + val name = MEName.text + + // match against fields + if (variable is PsiField) { + val qualifier = QualifiedMember.resolveQualifier(java) ?: variable.containingClass ?: return false + return context.getFields(name).any { it.matchField(variable, qualifier) } + } + + // match against local variables + val sourceArgs by lazy { + LocalVariables.guessLocalsAt(java, true, !PsiUtil.isAccessedForWriting(java)) + } + val sourceVariables by lazy { + LocalVariables.guessLocalsAt(java, false, !PsiUtil.isAccessedForWriting(java)) + } + for (localInfo in context.getLocalInfos(name)) { + val sourceLocals = if (localInfo.argsOnly) sourceArgs else sourceVariables + for (local in localInfo.matchSourceLocals(sourceLocals)) { + if (local.variable == variable) { + return true + } + } + } + + return false + } + + override fun getInputExprs() = emptyList() + + @Suppress("PropertyName") + protected abstract val MEName: MEName +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt new file mode 100644 index 000000000..50b02d3c7 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt @@ -0,0 +1,41 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENameMixin +import com.demonwav.mcdev.platform.mixin.expression.reference.MEDefinitionReference +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiReference + +abstract class MENameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MENameMixin { + override val isWildcard get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_WILDCARD + override val identifierElement get() = if (isWildcard) null else firstChild + + override fun getReference(): PsiReference? { + if (isWildcard) { + return null + } + return MEDefinitionReference(this as MEName) + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt new file mode 100644 index 000000000..1f72ada47 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt @@ -0,0 +1,138 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENewExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNewExpression +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.siblings + +abstract class MENewExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MENewExpressionMixin { + override val isArrayCreation get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) != null + + override val hasConstructorArguments get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_PAREN) != null + + override val dimensions get() = findChildrenByType(MEExpressionTypes.TOKEN_LEFT_BRACKET).size + + override val dimExprTokens: List get() { + val result = mutableListOf() + + var leftBracket: PsiElement? = findNotNullChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) + while (leftBracket != null) { + var expr: MEExpression? = null + var rightBracket: PsiElement? = null + var nextLeftBracket: PsiElement? = null + for (child in leftBracket.siblings(withSelf = false)) { + if (child is MEExpression) { + expr = child + } else { + when (child.node.elementType) { + MEExpressionTypes.TOKEN_RIGHT_BRACKET -> rightBracket = child + MEExpressionTypes.TOKEN_LEFT_BRACKET -> { + nextLeftBracket = child + break + } + } + } + } + result += MENewExpressionMixin.DimExprTokens(leftBracket, expr, rightBracket) + leftBracket = nextLeftBracket + } + + return result + } + + override val arrayInitializer get() = if (isArrayCreation) { + arguments + } else { + null + } + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiNewExpression) { + return false + } + + if (isArrayCreation) { + if (!java.isArrayCreation) { + return false + } + + val javaArrayType = java.type as? PsiArrayType ?: return false + if (javaArrayType.arrayDimensions != dimensions) { + return false + } + + val matchesType = context.project.meExpressionElementFactory.createType(type) + .matchesJava(javaArrayType.deepComponentType, context) + if (!matchesType) { + return false + } + + val javaArrayDims = java.arrayDimensions + val arrayDims = dimExprs + if (javaArrayDims.size != arrayDims.size) { + return false + } + if (!javaArrayDims.asSequence().zip(arrayDims.asSequence()).all { (javaArrayDim, arrayDim) -> + val actualJavaDim = PsiUtil.skipParenthesizedExprDown(javaArrayDim) ?: return@all false + arrayDim.matchesJava(actualJavaDim, context) + } + ) { + return false + } + + val javaArrayInitializer = java.arrayInitializer + val arrayInitializer = this.arrayInitializer + return if (javaArrayInitializer == null) { + arrayInitializer == null + } else { + arrayInitializer?.matchesJava(javaArrayInitializer.initializers, context) == true + } + } else { // !isArrayCreation + if (java.isArrayCreation) { + return false + } + + val javaType = java.type ?: return false + val javaArgs = java.argumentList ?: return false + + return context.project.meExpressionElementFactory.createType(type).matchesJava(javaType, context) && + arguments?.matchesJava(javaArgs, context) == true + } + } + + override fun getInputExprs() = dimExprs + (arguments?.expressionList ?: emptyList()) + + protected abstract val type: MEName + protected abstract val dimExprs: List + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt new file mode 100644 index 000000000..4061c6c6a --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt @@ -0,0 +1,37 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEParenthesizedExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return expression?.matchesJava(java, context) == true + } + + override fun getInputExprs() = listOfNotNull(expression) + + protected abstract val expression: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt new file mode 100644 index 000000000..0113a7b86 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt @@ -0,0 +1,43 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReturnStatement +import com.intellij.psi.util.PsiUtil + +abstract class MEReturnStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiReturnStatement) { + return false + } + val javaReturnValue = PsiUtil.skipParenthesizedExprDown(java.returnValue) ?: return false + return valueExpr?.matchesJava(javaReturnValue, context) == true + } + + override fun getInputExprs() = listOfNotNull(valueExpr) + + protected abstract val valueExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt new file mode 100644 index 000000000..39f8ad153 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEStatementImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEMatchableElement { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + throw UnsupportedOperationException("Please implement matchesJava for your statement type") + } + + override fun getInputExprs(): List { + throw UnsupportedOperationException("Please implement getInputExprs for your statement type") + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt new file mode 100644 index 000000000..7a578b242 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiModifier + +abstract class MEStaticMethodCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodCallExpression) { + return false + } + + val method = java.resolveMethod() ?: return false + if (!method.hasModifierProperty(PsiModifier.STATIC)) { + return false + } + + if (!memberName.isWildcard) { + val methodId = memberName.text + val qualifier = + QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false + if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) { + return false + } + } + + return arguments?.matchesJava(java.argumentList, context) == true + } + + override fun getInputExprs() = arguments?.expressionList ?: emptyList() + + protected abstract val memberName: MEName + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt new file mode 100644 index 000000000..2dee0b9c7 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodCallExpression +import com.siyeh.ig.psiutils.MethodCallUtils + +abstract class MESuperCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodCallExpression) { + return false + } + if (!MethodCallUtils.hasSuperQualifier(java)) { + return false + } + + val memberName = this.memberName ?: return false + if (!memberName.isWildcard) { + val method = java.resolveMethod() ?: return false + val methodId = memberName.text + val qualifier = + QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false + if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) { + return false + } + } + + return arguments?.matchesJava(java.argumentList, context) == true + } + + override fun getInputExprs() = arguments?.expressionList ?: emptyList() + + protected abstract val memberName: MEName? + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt new file mode 100644 index 000000000..6a4d25339 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt @@ -0,0 +1,36 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiThisExpression + +abstract class METhisExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return java is PsiThisExpression && java.qualifier == null + } + + override fun getInputExprs() = emptyList() +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt new file mode 100644 index 000000000..4226d24d3 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt @@ -0,0 +1,44 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiThrowStatement +import com.intellij.psi.util.PsiUtil + +abstract class METhrowStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiThrowStatement) { + return false + } + + val javaException = PsiUtil.skipParenthesizedExprDown(java.exception) ?: return false + return valueExpr?.matchesJava(javaException, context) == true + } + + override fun getInputExprs() = listOfNotNull(valueExpr) + + protected abstract val valueExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt new file mode 100644 index 000000000..f41550915 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt @@ -0,0 +1,53 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.METypeMixin +import com.demonwav.mcdev.util.descriptor +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiType + +abstract class METypeImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), METypeMixin { + override val isArray get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) != null + override val dimensions get() = findChildrenByType(MEExpressionTypes.TOKEN_LEFT_BRACKET).size + + override fun matchesJava(java: PsiType, context: MESourceMatchContext): Boolean { + if (MEName.isWildcard) { + return java.arrayDimensions >= dimensions + } else { + var unwrappedElementType = java + repeat(dimensions) { + unwrappedElementType = (unwrappedElementType as? PsiArrayType)?.componentType ?: return false + } + val descriptor = unwrappedElementType.descriptor + return context.getTypes(MEName.text).any { it == descriptor } + } + } + + @Suppress("PropertyName") + protected abstract val MEName: MEName +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt new file mode 100644 index 000000000..8238e13d8 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt @@ -0,0 +1,65 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEUnaryExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiUnaryExpression +import com.intellij.psi.util.PsiUtil + +abstract class MEUnaryExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEUnaryExpressionMixin { + override val operator get() = node.firstChildNode.elementType + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiUnaryExpression) { + return false + } + + val operatorMatches = when (java.operationTokenType) { + JavaTokenType.MINUS -> operator == MEExpressionTypes.TOKEN_MINUS + JavaTokenType.TILDE -> operator == MEExpressionTypes.TOKEN_BITWISE_NOT + else -> false + } + if (!operatorMatches) { + return false + } + + val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false + + if (operator == MEExpressionTypes.TOKEN_MINUS && javaOperand is PsiLiteral) { + // avoid matching "-1" etc + return false + } + + return expression?.matchesJava(javaOperand, context) == true + } + + override fun getInputExprs() = listOfNotNull(expression) + + protected abstract val expression: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt b/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt new file mode 100644 index 000000000..2b281f093 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt @@ -0,0 +1,70 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.reference + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReference +import com.intellij.psi.util.parentOfType +import com.intellij.util.ArrayUtilRt +import com.intellij.util.IncorrectOperationException + +class MEDefinitionReference(private var name: MEName) : PsiReference { + override fun getElement() = name + + override fun getRangeInElement() = TextRange(0, name.textLength) + + override fun resolve(): PsiElement? { + val file = element.parentOfType() ?: return null + val name = element.text + for (declItem in file.declarations) { + val declaration = declItem.declaration + if (declaration?.name == name) { + return declaration + } + } + + return null + } + + override fun getCanonicalText(): String = name.text + + override fun handleElementRename(newElementName: String): PsiElement { + name = name.replace(name.project.meExpressionElementFactory.createName(newElementName)) as MEName + return name + } + + override fun bindToElement(element: PsiElement): PsiElement { + throw IncorrectOperationException() + } + + override fun isReferenceTo(element: PsiElement) = element.manager.areElementsEquivalent(element, resolve()) + + override fun isSoft() = false + + override fun getVariants(): Array { + return (name.containingFile as? MEExpressionFile)?.declarations?.mapNotNull { it.declaration }?.toTypedArray() + ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY + } +} diff --git a/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt b/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt new file mode 100644 index 000000000..c20296b7c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression.reference + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclaration +import com.intellij.lang.findUsages.FindUsagesProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNamedElement + +class MEExpressionFindUsagesProvider : FindUsagesProvider { + override fun canFindUsagesFor(psiElement: PsiElement) = psiElement is MEDeclaration + + override fun getHelpId(psiElement: PsiElement) = null + + override fun getType(element: PsiElement) = "Definition" + + override fun getDescriptiveName(element: PsiElement) = (element as? PsiNamedElement)?.name ?: "null" + + override fun getNodeText(element: PsiElement, useFullName: Boolean) = getDescriptiveName(element) +} diff --git a/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt b/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt index 7f359fd58..44ccb2c9a 100644 --- a/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt +++ b/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt @@ -56,5 +56,15 @@ class MixinFoldingOptionsProvider : { settings.state.foldAccessorMethodCalls }, { b -> settings.state.foldAccessorMethodCalls = b }, ) + checkBox( + "Fold MixinExtras expression definitions", + { settings.state.foldDefinitions }, + { b -> settings.state.foldDefinitions = b }, + ) + checkBox( + "Fold MixinExtras expression definition fields and methods", + { settings.state.foldDefinitionFieldsAndMethods }, + { b -> settings.state.foldDefinitionFieldsAndMethods = b }, + ) } } diff --git a/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt b/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt index 081cc03aa..61fee79af 100644 --- a/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt +++ b/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt @@ -35,6 +35,8 @@ class MixinFoldingSettings : PersistentStateComponent) companion object { @@ -217,4 +220,6 @@ object DefaultInjectorAnnotationHandler : InjectorAnnotationHandler() { ) = null override val isSoft = true + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index d34c4aad2..69a4197e6 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -30,6 +30,7 @@ import com.demonwav.mcdev.util.descriptor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiMethod +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -135,4 +136,6 @@ class ModifyArgHandler : InjectorAnnotationHandler() { } } } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARG } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt index 446e9a20f..0d3b5476e 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodInsnNode @@ -58,4 +59,6 @@ class ModifyArgsHandler : InjectorAnnotationHandler() { ), ) } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARGS } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index eebd528de..839d9f832 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -31,6 +31,7 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes import com.intellij.psi.util.parentOfType +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -131,4 +132,6 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { override fun isInsnAllowed(insn: AbstractInsnNode): Boolean { return insn.opcode in allowedOpcodes } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_CONSTANT } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index d9e4eeae9..42398c23d 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Type import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -85,4 +86,6 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { return result } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_VARIABLE } diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt index 81e494291..70d2d7fd2 100644 --- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt @@ -39,6 +39,7 @@ import com.intellij.psi.PsiElementFactory import com.intellij.psi.PsiManager import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -105,6 +106,8 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { override val allowCoerce = true + override val mixinExtrasExpressionContextType = ExpressionContext.Type.REDIRECT + private interface RedirectType { fun isInsnAllowed(node: AbstractInsnNode) = true diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt index c1b1df6ac..ec61f4a4c 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt @@ -210,6 +210,7 @@ class AtResolver( val targetPsiClass = targetElement.parentOfType() ?: return emptyList() val navigationVisitor = injectionPoint.createNavigationVisitor(at, target, targetPsiClass) ?: return emptyList() + navigationVisitor.configureBytecodeTarget(targetClass, targetMethod) targetElement.accept(navigationVisitor) return bytecodeResults.mapNotNull { bytecodeResult -> diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt index 8939f36ae..17a917eef 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt @@ -321,6 +321,9 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { result += element } + open fun configureBytecodeTarget(classNode: ClassNode, methodNode: MethodNode) { + } + open fun visitStart(executableElement: PsiElement) { } @@ -407,6 +410,7 @@ abstract class CollectVisitor(protected val mode: Mode) { insn: AbstractInsnNode, element: T, qualifier: String? = null, + decorations: Map = emptyMap(), ) { // apply shift. // being able to break out of the shift loops is important to prevent IDE freezes in case of large shift bys. @@ -427,7 +431,14 @@ abstract class CollectVisitor(protected val mode: Mode) { } } - val result = Result(nextIndex++, insn, shiftedInsn ?: return, element, qualifier) + val result = Result( + nextIndex++, + insn, + shiftedInsn ?: return, + element, + qualifier, + if (insn === shiftedInsn) decorations else emptyMap() + ) var isFiltered = false for ((name, filter) in resultFilters) { if (!filter(result, method)) { @@ -463,6 +474,7 @@ abstract class CollectVisitor(protected val mode: Mode) { val insn: AbstractInsnNode, val target: T, val qualifier: String? = null, + val decorations: Map ) enum class Mode { MATCH_ALL, MATCH_FIRST, COMPLETION } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt index 4e7378045..7ad828179 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt @@ -28,7 +28,6 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MODIFY_ import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule -import com.demonwav.mcdev.util.isErasureEquivalentTo import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.openapi.module.Module import com.intellij.psi.JavaPsiFacade @@ -166,13 +165,13 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio val parentExpr = PsiUtil.skipParenthesizedExprUp(expression.parent) val isIincUnary = parentExpr is PsiUnaryExpression && ( - parentExpr.operationSign.tokenType == JavaTokenType.PLUSPLUS || - parentExpr.operationSign.tokenType == JavaTokenType.MINUSMINUS + parentExpr.operationTokenType == JavaTokenType.PLUSPLUS || + parentExpr.operationTokenType == JavaTokenType.MINUSMINUS ) val isIincAssignment = parentExpr is PsiAssignmentExpression && ( - parentExpr.operationSign.tokenType == JavaTokenType.PLUSEQ || - parentExpr.operationSign.tokenType == JavaTokenType.MINUSEQ + parentExpr.operationTokenType == JavaTokenType.PLUSEQ || + parentExpr.operationTokenType == JavaTokenType.MINUSEQ ) && PsiUtil.isConstantExpression(parentExpr.rExpression) && (parentExpr.rExpression?.constantValue as? Number)?.toInt() @@ -239,42 +238,10 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio name: String, localsHere: List, ) { - if (info.ordinal != null) { - val local = localsHere.asSequence().filter { - it.type.isErasureEquivalentTo(info.type) - }.drop(info.ordinal).firstOrNull() - if (name == local?.name) { + for (local in info.matchSourceLocals(localsHere)) { + if (name == local.name) { addResult(location) } - return - } - - if (info.index != null) { - val local = localsHere.getOrNull(info.index) - if (name == local?.name) { - addResult(location) - } - return - } - - if (info.names.isNotEmpty()) { - val matchingLocals = localsHere.filter { - info.names.contains(it.mixinName) - } - for (local in matchingLocals) { - if (local.name == name) { - addResult(location) - } - } - return - } - - // implicit mode - val local = localsHere.singleOrNull { - it.type.isErasureEquivalentTo(info.type) - } - if (local != null && local.name == name) { - addResult(location) } } } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt new file mode 100644 index 000000000..680d8ea19 --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt @@ -0,0 +1,284 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.handlers.mixinextras + +import com.demonwav.mcdev.platform.mixin.expression.IdentifierPoolFactory +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECapturingExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NavigationVisitor +import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.computeStringArray +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.findAnnotations +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.findMultiInjectionHost +import com.demonwav.mcdev.util.ifEmpty +import com.demonwav.mcdev.util.parseArray +import com.demonwav.mcdev.util.resolveType +import com.demonwav.mcdev.util.resolveTypeArray +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiModifierList +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.util.parentOfType +import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import java.util.IdentityHashMap +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode + +class ExpressionInjectionPoint : InjectionPoint() { + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + val modifierList = reference.findContainingModifierList() ?: return + if (modifierList.hasAnnotation(MixinConstants.MixinExtras.EXPRESSION)) { + return + } + + val project = reference.project + + val exprAnnotation = modifierList.addAfter( + JavaPsiFacade.getElementFactory(project) + .createAnnotationFromText("@${MixinConstants.MixinExtras.EXPRESSION}(\"\")", reference), + null + ) + + // add imports and reformat + JavaCodeStyleManager.getInstance(project).shortenClassReferences(exprAnnotation) + JavaCodeStyleManager.getInstance(project).optimizeImports(modifierList.containingFile) + val formattedModifierList = CodeStyleManager.getInstance(project).reformat(modifierList) as PsiModifierList + + // move the caret to @Expression("") + val formattedExprAnnotation = formattedModifierList.findAnnotation(MixinConstants.MixinExtras.EXPRESSION) + ?: return + val exprLiteral = formattedExprAnnotation.findDeclaredAttributeValue(null) ?: return + editor.caretModel.moveToOffset(exprLiteral.textRange.startOffset + 1) + } + + override fun createNavigationVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: PsiClass + ): NavigationVisitor? { + val project = at.project + + val atId = at.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null + val modifierList = injectorAnnotation.parent as? PsiModifierList ?: return null + val parsedExprs = parseExpressions(project, modifierList, atId) + parsedExprs.ifEmpty { return null } + + val sourceMatchContext = createSourceMatchContext(project, modifierList) + + return MyNavigationVisitor(parsedExprs.map { it.second }, sourceMatchContext) + } + + private fun createSourceMatchContext( + project: Project, + modifierList: PsiModifierList + ): MESourceMatchContext { + val matchContext = MESourceMatchContext(project) + + for (annotation in modifierList.annotations) { + if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + continue + } + + val definitionId = annotation.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList() + for (field in fields) { + val fieldRef = MemberReference.parse(field) ?: continue + matchContext.addField(definitionId, fieldRef) + } + + val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList() + for (method in methods) { + val methodRef = MemberReference.parse(method) ?: continue + matchContext.addMethod(definitionId, methodRef) + } + + val types = annotation.findDeclaredAttributeValue("type")?.resolveTypeArray() ?: emptyList() + for (type in types) { + matchContext.addType(definitionId, type.descriptor) + } + + val locals = annotation.findDeclaredAttributeValue("local")?.findAnnotations() ?: emptyList() + for (localAnnotation in locals) { + val localType = localAnnotation.findDeclaredAttributeValue("type")?.resolveType() + val localInfo = LocalInfo.fromAnnotation(localType, localAnnotation) + matchContext.addLocalInfo(definitionId, localInfo) + } + } + + return matchContext + } + + override fun doCreateCollectVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: ClassNode, + mode: CollectVisitor.Mode + ): CollectVisitor? { + val project = at.project + + val atId = at.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val contextType = MEExpressionMatchUtil.getContextType(project, at.parentOfType()?.qualifiedName) + + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null + val modifierList = injectorAnnotation.parent as? PsiModifierList ?: return null + val parsedExprs = parseExpressions(project, modifierList, atId) + parsedExprs.ifEmpty { return null } + + val module = at.findModule() ?: return null + + val poolFactory = MEExpressionMatchUtil.createIdentifierPoolFactory(module, targetClass, modifierList) + + return MyCollectVisitor(mode, project, targetClass, parsedExprs, poolFactory, contextType) + } + + private fun parseExpressions( + project: Project, + modifierList: PsiModifierList, + atId: String + ): List> { + return modifierList.annotations.asSequence() + .filter { exprAnnotation -> + exprAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION) && + (exprAnnotation.findDeclaredAttributeValue("id")?.constantStringValue ?: "") == atId + } + .flatMap { exprAnnotation -> + val expressionElements = exprAnnotation.findDeclaredAttributeValue("value")?.parseArray { it } + ?: return@flatMap emptySequence>() + expressionElements.asSequence().mapNotNull { expressionElement -> + val text = expressionElement.constantStringValue ?: return@mapNotNull null + val rootStatementPsi = InjectedLanguageManager.getInstance(project) + .getInjectedPsiFiles(expressionElement)?.firstOrNull() + ?.let { + (it.first as? MEExpressionFile)?.statements?.firstOrNull { stmt -> + stmt.findMultiInjectionHost()?.parentOfType() == exprAnnotation + } + } + ?: project.meExpressionElementFactory.createFile("do {$text}").statements.singleOrNull() + ?: project.meExpressionElementFactory.createStatement("empty") + MEExpressionMatchUtil.createExpression(text)?.let { it to rootStatementPsi } + } + } + .toList() + } + + override fun createLookup( + targetClass: ClassNode, + result: CollectVisitor.Result + ): LookupElementBuilder? { + return null + } + + private class MyCollectVisitor( + mode: Mode, + private val project: Project, + private val targetClass: ClassNode, + private val expressions: List>, + private val poolFactory: IdentifierPoolFactory, + private val contextType: ExpressionContext.Type, + ) : CollectVisitor(mode) { + override fun accept(methodNode: MethodNode) { + val insns = methodNode.instructions ?: return + + val pool = poolFactory(methodNode) + val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, methodNode) ?: return + + val result = IdentityHashMap>>() + + for ((expr, psiExpr) in expressions) { + MEExpressionMatchUtil.findMatchingInstructions( + targetClass, + methodNode, + pool, + flows, + expr, + flows.keys, + contextType, + false + ) { match -> + val capturedExpr = psiExpr.findElementAt(match.startOffset) + ?.parentOfType(withSelf = true) + ?.expression + ?: psiExpr + result.putIfAbsent(match.originalInsn, capturedExpr to match.decorations) + } + } + + if (result.isEmpty()) { + return + } + + for (insn in insns) { + val (element, decorations) = result[insn] ?: continue + addResult(insn, element, decorations = decorations) + } + } + } + + private class MyNavigationVisitor( + private val statements: List, + private val matchContext: MESourceMatchContext + ) : NavigationVisitor() { + override fun visitElement(element: PsiElement) { + for (statement in statements) { + if (statement.matchesJava(element, matchContext)) { + if (matchContext.captures.isNotEmpty()) { + for (capture in matchContext.captures) { + addResult(capture) + } + } else { + addResult(element) + } + } + matchContext.reset() + } + + super.visitElement(element) + } + } +} diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt index a0cb1ba40..2e76bcb9b 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt @@ -35,8 +35,10 @@ import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.toJavaIdentifier import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -52,30 +54,43 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( enum class InstructionType { METHOD_CALL { - override fun matches(insn: AbstractInsnNode) = insn is MethodInsnNode && insn.name != "" + override fun matches(target: TargetInsn) = target.insn is MethodInsnNode && target.insn.name != "" }, FIELD_GET { - override fun matches(insn: AbstractInsnNode) = - insn.opcode == Opcodes.GETFIELD || insn.opcode == Opcodes.GETSTATIC + override fun matches(target: TargetInsn) = + target.insn.opcode == Opcodes.GETFIELD || target.insn.opcode == Opcodes.GETSTATIC }, FIELD_SET { - override fun matches(insn: AbstractInsnNode) = - insn.opcode == Opcodes.PUTFIELD || insn.opcode == Opcodes.PUTSTATIC + override fun matches(target: TargetInsn) = + target.insn.opcode == Opcodes.PUTFIELD || target.insn.opcode == Opcodes.PUTSTATIC }, INSTANTIATION { - override fun matches(insn: AbstractInsnNode) = insn.opcode == Opcodes.NEW + override fun matches(target: TargetInsn) = target.insn.opcode == Opcodes.NEW }, INSTANCEOF { - override fun matches(insn: AbstractInsnNode) = insn.opcode == Opcodes.INSTANCEOF + override fun matches(target: TargetInsn) = target.insn.opcode == Opcodes.INSTANCEOF }, CONSTANT { - override fun matches(insn: AbstractInsnNode) = isConstant(insn) + override fun matches(target: TargetInsn) = isConstant(target.insn) }, RETURN { - override fun matches(insn: AbstractInsnNode) = insn.opcode in Opcodes.IRETURN..Opcodes.ARETURN + override fun matches(target: TargetInsn) = target.insn.opcode in Opcodes.IRETURN..Opcodes.ARETURN + }, + SIMPLE_OPERATION { + override fun matches(target: TargetInsn) = + target.hasDecoration(ExpressionDecorations.SIMPLE_OPERATION_ARGS) && + target.hasDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) + }, + SIMPLE_EXPRESSION { + override fun matches(target: TargetInsn) = + target.hasDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) + }, + STRING_CONCAT_EXPRESSION { + override fun matches(target: TargetInsn) = + target.hasDecoration(ExpressionDecorations.IS_STRING_CONCAT_EXPRESSION) }; - abstract fun matches(insn: AbstractInsnNode): Boolean + abstract fun matches(target: TargetInsn): Boolean } abstract val supportedInstructionTypes: Collection @@ -86,9 +101,13 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn, ): Pair? + open fun intLikeTypePositions( + target: TargetInsn + ): List = emptyList() + override val allowCoerce = true override fun expectedMethodSignature( @@ -98,26 +117,82 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( ): List? { val insns = resolveInstructions(annotation, targetClass, targetMethod) .ifEmpty { return emptyList() } - .map { it.insn } + .map { TargetInsn(it.insn, it.decorations) } if (insns.any { insn -> supportedInstructionTypes.none { it.matches(insn) } }) return emptyList() - val signatures = insns.map { expectedMethodSignature(annotation, targetClass, targetMethod, it) } + val signatures = insns.map { insn -> + expectedMethodSignature(annotation, targetClass, targetMethod, insn) + } val firstMatch = signatures[0] ?: return emptyList() if (signatures.drop(1).any { it != firstMatch }) return emptyList() - return listOf( - MethodSignature( - listOf( - firstMatch.first, - ParameterGroup( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - required = ParameterGroup.RequiredLevel.OPTIONAL, - isVarargs = true, - ), - ), - firstMatch.second - ) + val intLikeTypePositions = insns.map { intLikeTypePositions(it) }.distinct().singleOrNull().orEmpty() + return allPossibleSignatures( + annotation, + targetClass, + targetMethod, + firstMatch.first, + firstMatch.second, + intLikeTypePositions ) } + private fun allPossibleSignatures( + annotation: PsiAnnotation, + targetClass: ClassNode, + targetMethod: MethodNode, + params: ParameterGroup, + returnType: PsiType, + intLikeTypePositions: List + ): List { + if (intLikeTypePositions.isEmpty()) { + return listOf( + makeSignature(annotation, targetClass, targetMethod, params, returnType, intLikeTypePositions) + ) + } + return buildList { + for (actualType in intLikePsiTypes) { + val newParams = params.parameters.toMutableList() + var newReturnType = returnType + for (pos in intLikeTypePositions) { + when (pos) { + is MethodSignature.TypePosition.Return -> newReturnType = actualType + is MethodSignature.TypePosition.Param -> + newParams[pos.index] = newParams[pos.index].copy(type = actualType) + } + } + add( + makeSignature( + annotation, + targetClass, + targetMethod, + ParameterGroup(newParams), + newReturnType, + intLikeTypePositions + ) + ) + } + } + } + + private fun makeSignature( + annotation: PsiAnnotation, + targetClass: ClassNode, + targetMethod: MethodNode, + params: ParameterGroup, + returnType: PsiType, + intLikeTypePositions: List + ) = MethodSignature( + listOf( + params, + ParameterGroup( + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), + required = ParameterGroup.RequiredLevel.OPTIONAL, + isVarargs = true, + ), + ), + returnType, + intLikeTypePositions + ) + protected fun getInsnReturnType(insn: AbstractInsnNode): Type? { return when { insn is MethodInsnNode -> Type.getReturnType(insn.desc) @@ -287,7 +362,12 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( } else -> null - } ?: getInsnArgTypes(insn, targetClass)?.map { Parameter(null, it.toPsiType(elementFactory)) } + } ?: getInsnArgTypes(insn, targetClass)?.toParameters(annotation) + } + + protected fun List.toParameters(context: PsiElement, names: Array? = null): List { + val elementFactory = JavaPsiFacade.getElementFactory(context.project) + return mapIndexed { i, it -> Parameter(names?.getOrNull(i), it.toPsiType(elementFactory)) } } } @@ -348,3 +428,7 @@ private fun getConstantType(insn: AbstractInsnNode?): Type? { } } } + +private val intLikePsiTypes = listOf( + PsiTypes.intType(), PsiTypes.booleanType(), PsiTypes.charType(), PsiTypes.byteType(), PsiTypes.shortType() +) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt index 791423584..f16bf4924 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt @@ -20,10 +20,16 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras +import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup +import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.Parameter +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -31,7 +37,8 @@ import org.objectweb.asm.tree.MethodNode class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { override val supportedInstructionTypes = listOf( - InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.INSTANTIATION, InstructionType.CONSTANT + InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.INSTANTIATION, InstructionType.CONSTANT, + InstructionType.SIMPLE_EXPRESSION, InstructionType.STRING_CONCAT_EXPRESSION ) override fun extraTargetRestrictions(insn: AbstractInsnNode): Boolean { @@ -43,9 +50,36 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val psiType = getPsiReturnType(insn, annotation) ?: return null + val psiType = getReturnType(target, annotation) ?: return null return ParameterGroup(listOf(Parameter("original", psiType))) to psiType } + + override fun intLikeTypePositions(target: TargetInsn): List { + val expressionType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) + if (expressionType == ExpressionASMUtils.INTLIKE_TYPE) { + return listOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0)) + } + return emptyList() + } + + private fun getReturnType( + target: TargetInsn, + annotation: PsiAnnotation + ): PsiType? { + if (target.hasDecoration(ExpressionDecorations.IS_STRING_CONCAT_EXPRESSION)) { + return PsiType.getJavaLangString(annotation.manager, annotation.resolveScope) + } + val psiReturnType = getPsiReturnType(target.insn, annotation) + val rawReturnType = getInsnReturnType(target.insn) + val exprType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) + if (exprType != null && rawReturnType != exprType) { + // The expression knows more than the standard logic does. + return exprType.toPsiType(JavaPsiFacade.getElementFactory(annotation.project)) + } + return psiReturnType + } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_EXPRESSION_VALUE } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt index 0c3c3c564..38ec7fc8a 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -44,9 +45,11 @@ class ModifyReceiverHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val params = getPsiParameters(insn, targetClass, annotation) ?: return null + val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null return ParameterGroup(params) to params[0].type } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RECEIVER } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt index 8c2706c33..df9157186 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt @@ -25,7 +25,7 @@ import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType -import org.objectweb.asm.tree.AbstractInsnNode +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -36,9 +36,11 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode - ): Pair? { + target: TargetInsn + ): Pair { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) return ParameterGroup(listOf(Parameter("original", returnType))) to returnType } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RETURN_VALUE } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt new file mode 100644 index 000000000..1a03ff012 --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.handlers.mixinextras + +import org.objectweb.asm.tree.AbstractInsnNode + +class TargetInsn(val insn: AbstractInsnNode, private val decorations: Map) { + fun hasDecoration(key: String) = key in decorations + + @Suppress("UNCHECKED_CAST") + fun getDecoration(key: String): T? = decorations[key] as T +} diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index cd2011c19..938f0103e 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -31,6 +31,7 @@ import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement import com.intellij.psi.search.GlobalSearchScope +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -80,4 +81,6 @@ class WrapMethodHandler : InjectorAnnotationHandler() { canDecompile = true )?.let(::listOf).orEmpty() } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index a5e2242e8..ef1726cc8 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt @@ -20,19 +20,25 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras +import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType +import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.Parameter +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType -import org.objectweb.asm.tree.AbstractInsnNode +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations +import org.objectweb.asm.Type import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { override val supportedInstructionTypes = listOf( InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.FIELD_SET, InstructionType.INSTANCEOF, - InstructionType.INSTANTIATION + InstructionType.INSTANTIATION, InstructionType.SIMPLE_OPERATION ) override fun getAtKey(annotation: PsiAnnotation): String { @@ -43,13 +49,51 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val params = getPsiParameters(insn, targetClass, annotation) ?: return null - val returnType = getPsiReturnType(insn, annotation) ?: return null + val params = getParameterTypes(target, targetClass, annotation) ?: return null + val returnType = getReturnType(target, annotation) ?: return null val operationType = mixinExtrasOperationType(annotation, returnType) ?: return null return ParameterGroup( params + Parameter("original", operationType) ) to returnType } + + override fun intLikeTypePositions(target: TargetInsn) = buildList { + if ( + target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) + == ExpressionASMUtils.INTLIKE_TYPE + ) { + add(MethodSignature.TypePosition.Return) + } + target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS)?.forEachIndexed { i, it -> + if (it == ExpressionASMUtils.INTLIKE_TYPE) { + add(MethodSignature.TypePosition.Param(i)) + } + } + } + + private fun getParameterTypes( + target: TargetInsn, + targetClass: ClassNode, + annotation: PsiAnnotation + ): List? { + getPsiParameters(target.insn, targetClass, annotation)?.let { return it } + val args = target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS) ?: return null + return args.toList().toParameters( + annotation, + target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_PARAM_NAMES) + ) + } + + private fun getReturnType( + target: TargetInsn, + annotation: PsiAnnotation + ): PsiType? { + getPsiReturnType(target.insn, annotation)?.let { return it } + val type = target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) ?: return null + return type.toPsiType(JavaPsiFacade.getElementFactory(annotation.project)) + } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_OPERATION } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt index 8a92d6bc6..df64324b7 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -42,9 +43,11 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val params = getPsiParameters(insn, targetClass, annotation) ?: return null + val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null return ParameterGroup(params) to PsiTypes.booleanType() } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_WITH_CONDITION } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index a51fd6db3..bc168040a 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -20,7 +20,6 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector -import com.demonwav.mcdev.platform.mixin.handlers.InjectAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection @@ -34,20 +33,35 @@ import com.demonwav.mcdev.platform.mixin.util.isConstructor import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.fullQualifiedName -import com.demonwav.mcdev.util.normalize +import com.demonwav.mcdev.util.invokeLater import com.demonwav.mcdev.util.synchronize +import com.intellij.codeInsight.FileModificationService import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview import com.intellij.codeInsight.intention.QuickFixFactory -import com.intellij.codeInspection.LocalQuickFix -import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.template.Expression +import com.intellij.codeInsight.template.ExpressionContext +import com.intellij.codeInsight.template.Template +import com.intellij.codeInsight.template.TemplateBuilderImpl +import com.intellij.codeInsight.template.TemplateManager +import com.intellij.codeInsight.template.TextResult +import com.intellij.codeInsight.template.impl.VariableNode +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange import com.intellij.psi.JavaElementVisitor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiEllipsisType +import com.intellij.psi.PsiFile import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifier import com.intellij.psi.PsiNameHelper @@ -57,6 +71,9 @@ import com.intellij.psi.PsiType import com.intellij.psi.codeStyle.JavaCodeStyleManager import com.intellij.psi.codeStyle.VariableKind import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.TypeConversionUtil +import com.intellij.psi.util.parentOfType +import com.intellij.refactoring.suggested.startOffset import org.objectweb.asm.Opcodes class InvalidInjectorMethodSignatureInspection : MixinInspection() { @@ -166,50 +183,42 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } if (!isValid) { - val (expectedParameters, expectedReturnType) = possibleSignatures[0] - - val checkResult = checkParameters(parameters, expectedParameters, handler.allowCoerce) - if (checkResult != CheckResult.OK) { - reportedSignature = true - - val description = - "Method parameters do not match expected parameters for $annotationName" - val quickFix = ParametersQuickFix( - expectedParameters, - handler is InjectAnnotationHandler, - ) - if (checkResult == CheckResult.ERROR) { - holder.registerProblem(parameters, description, quickFix) - } else { - holder.registerProblem( - parameters, - description, - ProblemHighlightType.WARNING, - quickFix, - ) - } + val (expectedParameters, expectedReturnType, intLikeTypePositions) = possibleSignatures[0] + val normalizedReturnType = when (expectedReturnType) { + is PsiEllipsisType -> expectedReturnType.toArrayType() + else -> expectedReturnType } + val paramsCheck = checkParameters(parameters, expectedParameters, handler.allowCoerce) + val isWarning = paramsCheck == CheckResult.WARNING val methodReturnType = method.returnType - if (methodReturnType == null || - !checkReturnType(expectedReturnType, methodReturnType, method, handler.allowCoerce) - ) { + val returnTypeOk = methodReturnType != null && + checkReturnType(normalizedReturnType, methodReturnType, method, handler.allowCoerce) + val isError = paramsCheck == CheckResult.ERROR || !returnTypeOk + if (isWarning || isError) { reportedSignature = true - val normalizedExpected = when (expectedReturnType) { - is PsiEllipsisType -> expectedReturnType.toArrayType() - else -> expectedReturnType - } - + val description = + "Method signature does not match expected signature for $annotationName" + val quickFix = SignatureQuickFix( + method, + expectedParameters.takeUnless { paramsCheck == CheckResult.OK }, + normalizedReturnType.takeUnless { returnTypeOk }, + intLikeTypePositions + ) + val highlightType = + if (isError) + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + else + ProblemHighlightType.WARNING + val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent + val declarationEnd = method.parameterList.textRangeInParent.endOffset holder.registerProblem( - method.returnTypeElement ?: identifier, - "Expected return type '${normalizedExpected.presentableText}' " + - "for $annotationName method", - QuickFixFactory.getInstance().createMethodReturnFix( - method, - normalizedExpected, - false, - ), + method, + description, + highlightType, + TextRange.create(declarationStart, declarationEnd), + quickFix ) } } @@ -224,9 +233,9 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { method: PsiMethod, allowCoerce: Boolean, ): Boolean { - val normalizedExpected = expectedReturnType.normalize() - val normalizedReturn = methodReturnType.normalize() - if (normalizedExpected == normalizedReturn) { + val expectedErasure = TypeConversionUtil.erasure(expectedReturnType) + val returnErasure = TypeConversionUtil.erasure(methodReturnType) + if (expectedErasure == returnErasure) { return true } if (!allowCoerce || !method.hasAnnotation(COERCE)) { @@ -289,22 +298,43 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { OK, WARNING, ERROR } - private class ParametersQuickFix( + private class SignatureQuickFix( + method: PsiMethod, @SafeFieldForPreview - private val expected: List, - isInject: Boolean, - ) : LocalQuickFix { - - private val fixName = if (isInject) { - "Fix method parameters" - } else { - "Fix method parameters (won't keep captured locals)" - } + private val expectedParams: List?, + @SafeFieldForPreview + private val expectedReturnType: PsiType?, + private val intLikeTypePositions: List + ) : LocalQuickFixAndIntentionActionOnPsiElement(method) { + + private val fixName = "Fix method signature" override fun getFamilyName() = fixName - override fun applyFix(project: Project, descriptor: ProblemDescriptor) { - val parameters = descriptor.psiElement as PsiParameterList + override fun getText() = familyName + + override fun startInWriteAction() = false + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement, + ) { + if (!FileModificationService.getInstance().preparePsiElementForWrite(startElement)) { + return + } + val method = startElement as PsiMethod + fixParameters(project, method.parameterList) + fixReturnType(method) + fixIntLikeTypes(method, editor ?: return) + } + + private fun fixParameters(project: Project, parameters: PsiParameterList) { + if (expectedParams == null) { + return + } // We want to preserve captured locals val locals = parameters.parameters.dropWhile { val fqname = (it.type as? PsiClassType)?.fullQualifiedName ?: return@dropWhile true @@ -316,7 +346,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { // We want to preserve sugars, and while we're at it, we might as well move them all to the end val sugars = parameters.parameters.filter { it.isMixinExtrasSugar } - val newParams = expected.flatMapTo(mutableListOf()) { + val newParams = expectedParams.flatMapTo(mutableListOf()) { if (it.default) { val nameHelper = PsiNameHelper.getInstance(project) val languageLevel = PsiUtil.getLanguageLevel(parameters) @@ -335,7 +365,81 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { // Restore the captured locals and sugars before applying the fix newParams.addAll(locals) newParams.addAll(sugars) - parameters.synchronize(newParams) + runWriteAction { + parameters.synchronize(newParams) + } } + + private fun fixReturnType(method: PsiMethod) { + if (expectedReturnType == null) { + return + } + QuickFixFactory.getInstance() + .createMethodReturnFix(method, expectedReturnType, false) + .applyFix() + } + + private fun fixIntLikeTypes(method: PsiMethod, editor: Editor) { + if (intLikeTypePositions.isEmpty()) { + return + } + invokeLater { + WriteCommandAction.runWriteCommandAction( + method.project, + "Choose Int-Like Type", + null, + { + val template = makeIntLikeTypeTemplate(method, intLikeTypePositions) + if (template != null) { + editor.caretModel.moveToOffset(method.startOffset) + TemplateManager.getInstance(method.project) + .startTemplate(editor, template) + } + }, + method.parentOfType()!! + ) + } + } + + private fun makeIntLikeTypeTemplate( + method: PsiMethod, + positions: List + ): Template? { + val builder = TemplateBuilderImpl(method) + builder.replaceElement( + positions.first().getElement(method) ?: return null, + "intliketype", + ChooseIntLikeTypeExpression(), + true + ) + for (pos in positions.drop(1)) { + builder.replaceElement( + pos.getElement(method) ?: return null, + VariableNode("intliketype", null), + false + ) + } + return builder.buildInlineTemplate() + } + } +} + +private class ChooseIntLikeTypeExpression : Expression() { + private val lookupItems: Array = intLikeTypes.map(LookupElementBuilder::create).toTypedArray() + + override fun calculateLookupItems(context: ExpressionContext) = if (lookupItems.size > 1) lookupItems else null + + override fun calculateQuickResult(context: ExpressionContext) = calculateResult(context) + + override fun calculateResult(context: ExpressionContext) = TextResult("int") + + private companion object { + private val intLikeTypes = listOf( + "int", + "char", + "boolean", + "byte", + "short" + ) } } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 167782cbe..631e1acf9 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -20,6 +20,24 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector +import com.intellij.psi.PsiMethod import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeElement -data class MethodSignature(val parameters: List, val returnType: PsiType) +data class MethodSignature( + val parameters: List, + val returnType: PsiType, + val intLikeTypes: List = emptyList() +) { + sealed interface TypePosition { + fun getElement(method: PsiMethod): PsiTypeElement? + + data object Return : TypePosition { + override fun getElement(method: PsiMethod) = method.returnTypeElement + } + + data class Param(val index: Int) : TypePosition { + override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement + } + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt b/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt index 720a4084a..79ea61bfb 100644 --- a/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt +++ b/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt @@ -38,6 +38,7 @@ import com.intellij.psi.PsiInstanceOfExpression import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeCastExpression import com.intellij.psi.PsiTypeTestPattern +import com.intellij.psi.util.JavaPsiPatternUtil import com.intellij.psi.util.PsiUtil /** @@ -54,7 +55,8 @@ class MixinClassCastInspectionSuppressor : InspectionSuppressor { // check instanceof if (element is PsiInstanceOfExpression) { val castType = element.checkType?.type - ?: (element.pattern as? PsiTypeTestPattern)?.checkType?.type + ?: (JavaPsiPatternUtil.skipParenthesizedPatternDown(element.pattern) as? PsiTypeTestPattern) + ?.checkType?.type ?: return false var operand = PsiUtil.skipParenthesizedExprDown(element.operand) ?: return false while (operand is PsiTypeCastExpression) { diff --git a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt index 416eacdf7..58d68e12e 100644 --- a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt +++ b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt @@ -20,6 +20,8 @@ package com.demonwav.mcdev.platform.mixin.reference +import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReference +import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT import com.demonwav.mcdev.util.insideAnnotationAttribute @@ -65,5 +67,15 @@ class MixinReferenceContributor : PsiReferenceContributor() { InvokerReference.ELEMENT_PATTERN, InvokerReference, ) + + // Definition references + registrar.registerReferenceProvider( + FieldDefinitionReference.ELEMENT_PATTERN, + FieldDefinitionReference, + ) + registrar.registerReferenceProvider( + MethodDefinitionReference.ELEMENT_PATTERN, + MethodDefinitionReference, + ) } } diff --git a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt index 4e8d34f4f..88e912674 100644 --- a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt +++ b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt @@ -48,7 +48,6 @@ import com.demonwav.mcdev.util.resolveTypeArray import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.util.RecursionManager -import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation @@ -228,71 +227,7 @@ fun MemberReference.toMixinString(): String { } class MixinMemberParser : MixinSelectorParser { - override fun parse(value: String, context: PsiElement): MixinSelector? { - val reference = value.replace(" ", "") - val owner: String? - - var pos = reference.lastIndexOf('.') - if (pos != -1) { - // Everything before the dot is the qualifier/owner - owner = reference.substring(0, pos).replace('/', '.') - } else { - pos = reference.indexOf(';') - if (pos != -1 && reference.startsWith('L')) { - val internalOwner = reference.substring(1, pos) - if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) { - // Invalid: Qualifier should only contain slashes - return null - } - - owner = internalOwner.replace('/', '.') - - // if owner is all there is to the selector, match anything with the owner - if (pos == reference.length - 1) { - return MemberReference("", null, owner, matchAllNames = true, matchAllDescs = true) - } - } else { - // No owner/qualifier specified - pos = -1 - owner = null - } - } - - val descriptor: String? - val name: String - val matchAllNames = reference.getOrNull(pos + 1) == '*' - val matchAllDescs: Boolean - - // Find descriptor separator - val methodDescPos = reference.indexOf('(', pos + 1) - if (methodDescPos != -1) { - // Method descriptor - descriptor = reference.substring(methodDescPos) - name = reference.substring(pos + 1, methodDescPos) - matchAllDescs = false - } else { - val fieldDescPos = reference.indexOf(':', pos + 1) - if (fieldDescPos != -1) { - descriptor = reference.substring(fieldDescPos + 1) - name = reference.substring(pos + 1, fieldDescPos) - matchAllDescs = false - } else { - descriptor = null - matchAllDescs = reference.endsWith('*') - name = if (matchAllDescs) { - reference.substring(pos + 1, reference.lastIndex) - } else { - reference.substring(pos + 1) - } - } - } - - if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { - return null - } - - return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs) - } + override fun parse(value: String, context: PsiElement) = MemberReference.parse(value) } // Regex reference diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt new file mode 100644 index 000000000..4489eff4e --- /dev/null +++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt @@ -0,0 +1,45 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.target + +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.util.parentOfType + +class DefinitionReferenceGTDHandler : GotoDeclarationHandler { + override fun getGotoDeclarationTargets( + sourceElement: PsiElement?, + offset: Int, + editor: Editor? + ): Array? { + if (sourceElement == null) return null + val stringLiteral = sourceElement.parentOfType() ?: return null + if (FieldDefinitionReference.ELEMENT_PATTERN.accepts(stringLiteral)) { + return FieldDefinitionReference.resolveForNavigation(stringLiteral) + } + if (MethodDefinitionReference.ELEMENT_PATTERN.accepts(stringLiteral)) { + return MethodDefinitionReference.resolveForNavigation(stringLiteral) + } + return null + } +} diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt new file mode 100644 index 000000000..a6927b8a8 --- /dev/null +++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt @@ -0,0 +1,182 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.target + +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.reference.MixinReference +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findField +import com.demonwav.mcdev.util.findMethods +import com.demonwav.mcdev.util.insideAnnotationAttribute +import com.demonwav.mcdev.util.mapFirstNotNull +import com.demonwav.mcdev.util.mapToArray +import com.demonwav.mcdev.util.reference.PolyReferenceResolver +import com.demonwav.mcdev.util.toTypedArray +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.project.Project +import com.intellij.patterns.PsiJavaPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementResolveResult +import com.intellij.psi.PsiMember +import com.intellij.psi.ResolveResult +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.containers.sequenceOfNotNull +import com.llamalad7.mixinextras.expression.impl.flow.FlowValue +import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.LMFInfo +import com.llamalad7.mixinextras.expression.impl.utils.FlowDecorations +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.MethodInsnNode + +abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinReference { + abstract fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? + abstract fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass): Sequence + abstract fun referenceToString(memberReference: MemberReference): String + + override fun isUnresolved(context: PsiElement) = resolveInBytecode(context).isNotEmpty() + + override fun isValidAnnotation(name: String, project: Project) = name == MixinConstants.MixinExtras.DEFINITION + + override fun resolveReference(context: PsiElement): Array { + return resolveForNavigation(context).mapToArray(::PsiElementResolveResult) + } + + fun resolveForNavigation(context: PsiElement): Array { + val project = context.project + val facade = JavaPsiFacade.getInstance(project) + return resolveInBytecode(context).asSequence().flatMap { memberReference -> + val ownerClass = facade.findClass( + memberReference.owner!!.replace('$', '.'), + GlobalSearchScope.allScope(project) + ) ?: return@flatMap emptySequence() + getMatchesInClass(memberReference.withoutOwner, ownerClass) + }.toTypedArray() + } + + override fun collectVariants(context: PsiElement) = + resolveInBytecode( + context, + MemberReference("*", null, null, matchAllNames = true, matchAllDescs = true) + ).mapToArray { + LookupElementBuilder.create(referenceToString(it)) + .withPresentableText(it.presentableText) + .withLookupString(it.name) + } + + fun resolveInBytecode(context: PsiElement): List { + val memberReference = context.constantStringValue?.let(MemberReference::parse) ?: return emptyList() + return resolveInBytecode(context, memberReference) + } + + private fun resolveInBytecode(context: PsiElement, memberReference: MemberReference): List { + val project = context.project + val modifierList = context.findContainingModifierList() ?: return emptyList() + val (annotation, handler) = modifierList.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null + annotation to handler + } ?: return emptyList() + + val result = mutableListOf() + + for (target in handler.resolveTarget(annotation)) { + if (target !is MethodTargetMember) { + continue + } + + if (target.classAndMethod.method.instructions == null) { + continue + } + + val flow = MEExpressionMatchUtil.getFlowMap( + project, + target.classAndMethod.clazz, + target.classAndMethod.method + ) ?: continue + + for (node in flow.values) { + val fullReference = getFullReferenceIfMatches(memberReference, node) ?: continue + result += fullReference + } + } + + return result + } +} + +object FieldDefinitionReference : AbstractDefinitionReference() { + val ELEMENT_PATTERN = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) + .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "field") + + override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? { + val insn = node.insn + if (insn !is FieldInsnNode || !memberReference.matchField(insn.owner, insn.name, insn.desc)) { + return null + } + + return MemberReference(insn.name, insn.desc, insn.owner.replace('/', '.')) + } + + override fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass) = + sequenceOfNotNull(clazz.findField(memberReference, checkBases = true)) + + override fun referenceToString(memberReference: MemberReference) = + "L${memberReference.owner?.replace('.', '/')};${memberReference.name}:${memberReference.descriptor}" + + override val description = "defined field '%s'" +} + +object MethodDefinitionReference : AbstractDefinitionReference() { + val ELEMENT_PATTERN = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) + .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "method") + + override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? { + val info = node.getDecoration(FlowDecorations.LMF_INFO) + val insn = node.insn + val (owner, name, desc) = when { + info != null && (info.type == LMFInfo.Type.FREE_METHOD || info.type == LMFInfo.Type.BOUND_METHOD) -> + Triple(info.impl.owner, info.impl.name, info.impl.desc) + + insn is MethodInsnNode -> Triple(insn.owner, insn.name, insn.desc) + else -> return null + } + if (!memberReference.matchMethod(owner, name, desc)) { + return null + } + + return MemberReference(name, desc, owner.replace('/', '.')) + } + + override fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass) = + clazz.findMethods(memberReference, checkBases = true) + + override fun referenceToString(memberReference: MemberReference) = + "L${memberReference.owner?.replace('.', '/')};${memberReference.name}${memberReference.descriptor}" + + override val description = "defined method '%s'" +} diff --git a/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt b/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt index 3e800c643..842be4863 100644 --- a/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt @@ -41,8 +41,8 @@ import org.objectweb.asm.tree.analysis.SimpleVerifier object AsmDfaUtil { private val LOGGER = thisLogger() - fun analyzeMethod(project: Project, clazz: ClassNode, method: MethodNode): Array?>? { - return method.cached(clazz, project) { + fun analyzeMethod(project: Project, classIn: ClassNode, methodIn: MethodNode): Array?>? { + return methodIn.cached(classIn, project) { clazz, method -> try { Analyzer( PsiBytecodeInterpreter( diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt index da1de2c4a..981be7320 100644 --- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.util.findQualifiedClass import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.hasSyntheticMethod import com.demonwav.mcdev.util.isErasureEquivalentTo +import com.demonwav.mcdev.util.lockedCached import com.demonwav.mcdev.util.loggerForTopLevel import com.demonwav.mcdev.util.mapToArray import com.demonwav.mcdev.util.realName @@ -42,6 +43,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.Project import com.intellij.openapi.roots.CompilerModuleExtension +import com.intellij.openapi.util.Key import com.intellij.openapi.util.RecursionManager import com.intellij.psi.JavaPsiFacade import com.intellij.psi.JavaRecursiveElementWalkingVisitor @@ -67,19 +69,27 @@ import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiParameter import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes import com.intellij.psi.impl.compiled.ClsElementImpl import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.CachedValue import com.intellij.psi.util.PsiUtil import com.intellij.refactoring.util.LambdaRefactoringUtil import com.intellij.util.CommonJavaRefactoringUtil +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils +import java.io.PrintWriter +import java.io.StringWriter import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap import org.objectweb.asm.ClassReader import org.objectweb.asm.Handle import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.signature.SignatureReader import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.AnnotationNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.FieldInsnNode import org.objectweb.asm.tree.FieldNode @@ -89,6 +99,10 @@ import org.objectweb.asm.tree.InvokeDynamicInsnNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode import org.objectweb.asm.tree.VarInsnNode +import org.objectweb.asm.util.Textifier +import org.objectweb.asm.util.TraceAnnotationVisitor +import org.objectweb.asm.util.TraceClassVisitor +import org.objectweb.asm.util.TraceMethodVisitor private val LOGGER = loggerForTopLevel() @@ -129,10 +143,25 @@ private fun hasModifier(access: Int, @PsiModifier.ModifierConstant modifier: Str } fun Type.toPsiType(elementFactory: PsiElementFactory, context: PsiElement? = null): PsiType { + if (this == ExpressionASMUtils.INTLIKE_TYPE) { + return PsiTypes.intType() + } val javaClassName = className.replace("(\\$)(\\D)".toRegex()) { "." + it.groupValues[2] } return elementFactory.createTypeFromText(javaClassName, context) } +val Type.canonicalName get() = computeCanonicalName(this) + +private fun computeCanonicalName(type: Type): String { + return when (type.sort) { + Type.ARRAY -> computeCanonicalName(type.elementType) + "[]".repeat(type.dimensions) + Type.OBJECT -> type.className.replace('$', '.') + else -> type.className + } +} + +val Type.isPrimitive get() = sort != Type.ARRAY && sort != Type.OBJECT && sort != Type.METHOD + private fun hasAccess(access: Int, flag: Int) = (access and flag) != 0 // ClassNode @@ -152,13 +181,10 @@ private val LOAD_CLASS_FILE_BYTES: Method? = runCatching { .let { it.isAccessible = true; it } }.getOrNull() +private val INNER_CLASS_NODES_KEY = Key.create>>("mcdev.innerClassNodes") + /** * Tries to find the bytecode for the class for the given qualified name. - * - * ### Implementation note: - * First attempts to resolve the class using [findQualifiedClass]. This may fail in the case of anonymous classes, which - * don't exist inside `PsiCompiledElement`s, so it then creates a fake `PsiClass` based on the qualified name and - * attempts to resolve it from that. */ fun findClassNodeByQualifiedName(project: Project, module: Module?, fqn: String): ClassNode? { val psiClass = findQualifiedClass(project, fqn) @@ -166,52 +192,70 @@ fun findClassNodeByQualifiedName(project: Project, module: Module?, fqn: String) return findClassNodeByPsiClass(psiClass, module) } - // try to find it by a fake one - val fakeClassNode = ClassNode() - fakeClassNode.name = fqn.replace('.', '/') - val fakePsiClass = fakeClassNode.constructClass(project, "") ?: return null - return findClassNodeByPsiClass(fakePsiClass, module) + fun resolveViaFakeClass(): ClassNode? { + val fakeClassNode = ClassNode() + fakeClassNode.name = fqn.replace('.', '/') + val fakePsiClass = fakeClassNode.constructClass(project, "") ?: return null + return findClassNodeByPsiClass(fakePsiClass, module) + } + + val outerClass = findQualifiedClass(project, fqn.substringBefore('$')) + if (outerClass != null) { + val innerClasses = outerClass.lockedCached( + INNER_CLASS_NODES_KEY, + compute = ::ConcurrentHashMap + ) + return innerClasses.computeIfAbsent(fqn) { resolveViaFakeClass() } + } + + return resolveViaFakeClass() } +private val NODE_BY_PSI_CLASS_KEY = Key.create>("mcdev.nodeByPsiClass") + fun findClassNodeByPsiClass(psiClass: PsiClass, module: Module? = psiClass.findModule()): ClassNode? { - return try { - val bytes = LOAD_CLASS_FILE_BYTES?.invoke(null, psiClass) as? ByteArray - if (bytes == null) { - // find compiler output - if (module == null) return null - val fqn = psiClass.fullQualifiedName ?: return null - var parentDir = CompilerModuleExtension.getInstance(module)?.compilerOutputPath ?: return null - val packageName = fqn.substringBeforeLast('.', "") - if (packageName.isNotEmpty()) { - for (dir in packageName.split('.')) { - parentDir = parentDir.findChild(dir) ?: return null + return psiClass.lockedCached(NODE_BY_PSI_CLASS_KEY) { + try { + val bytes = LOAD_CLASS_FILE_BYTES?.invoke(null, psiClass) as? ByteArray + if (bytes == null) { + // find compiler output + if (module == null) return@lockedCached null + val fqn = psiClass.fullQualifiedName ?: return@lockedCached null + var parentDir = CompilerModuleExtension.getInstance(module)?.compilerOutputPath + ?: return@lockedCached null + val packageName = fqn.substringBeforeLast('.', "") + if (packageName.isNotEmpty()) { + for (dir in packageName.split('.')) { + parentDir = parentDir.findChild(dir) ?: return@lockedCached null + } } + val classFile = parentDir.findChild("${fqn.substringAfterLast('.')}.class") + ?: return@lockedCached null + val node = ClassNode() + classFile.inputStream.use { ClassReader(it).accept(node, 0) } + node + } else { + val node = ClassNode() + ClassReader(bytes).accept(node, 0) + node + } + } catch (e: Throwable) { + val actualThrowable = if (e is InvocationTargetException) e.cause ?: e else e + if (actualThrowable is ProcessCanceledException) { + throw actualThrowable } - val classFile = parentDir.findChild("${fqn.substringAfterLast('.')}.class") ?: return null - val node = ClassNode() - classFile.inputStream.use { ClassReader(it).accept(node, 0) } - node - } else { - val node = ClassNode() - ClassReader(bytes).accept(node, 0) - node - } - } catch (e: Throwable) { - val actualThrowable = if (e is InvocationTargetException) e.cause ?: e else e - if (actualThrowable is ProcessCanceledException) { - throw actualThrowable - } - if (actualThrowable is NoSuchFileException) { - return null - } + if (actualThrowable is NoSuchFileException) { + return@lockedCached null + } - val message = actualThrowable.message - // TODO: display an error to the user? - if (message == null || !message.contains("Unsupported class file major version")) { - LOGGER.error(actualThrowable) + val message = actualThrowable.message + // TODO: display an error to the user? + if (message == null || !message.contains("Unsupported class file major version")) { + LOGGER.error(actualThrowable) + } + null } - null } } @@ -325,8 +369,11 @@ private fun ClassNode.constructClass(project: Project, body: String): PsiClass? return clazz } -inline fun ClassNode.cached(project: Project, vararg dependencies: Any, crossinline compute: () -> T): T { - return findStubClass(project)?.cached(*dependencies, compute = compute) ?: compute() +fun ClassNode.cached(project: Project, vararg dependencies: Any, compute: (ClassNode) -> T): T { + val unsafeClass = UnsafeCachedValueCapture(this) + return findStubClass(project)?.cached(*dependencies) { + compute(unsafeClass.value) + } ?: compute(this) } /** @@ -452,13 +499,17 @@ fun FieldNode.getGenericType( return Type.getType(this.desc).toPsiType(elementFactory) } -inline fun FieldNode.cached( +fun FieldNode.cached( clazz: ClassNode, project: Project, vararg dependencies: Any, - crossinline compute: () -> T, + compute: (ClassNode, FieldNode) -> T, ): T { - return findStubField(clazz, project)?.cached(*dependencies, compute = compute) ?: compute() + val unsafeClass = UnsafeCachedValueCapture(clazz) + val unsafeField = UnsafeCachedValueCapture(this) + return findStubField(clazz, project)?.cached(*dependencies) { + compute(unsafeClass.value, unsafeField.value) + } ?: compute(clazz, this) } fun FieldNode.findStubField(clazz: ClassNode, project: Project): PsiField? { @@ -693,13 +744,17 @@ private fun findAssociatedLambda(psiClass: PsiClass, clazz: ClassNode, lambdaMet } } -inline fun MethodNode.cached( +fun MethodNode.cached( clazz: ClassNode, project: Project, vararg dependencies: Array, - crossinline compute: () -> T, + compute: (ClassNode, MethodNode) -> T, ): T { - return findStubMethod(clazz, project)?.cached(*dependencies, compute = compute) ?: compute() + val unsafeClass = UnsafeCachedValueCapture(clazz) + val unsafeMethod = UnsafeCachedValueCapture(this) + return findStubMethod(clazz, project)?.cached(*dependencies) { + compute(unsafeClass.value, unsafeMethod.value) + } ?: compute(clazz, this) } fun MethodNode.findStubMethod(clazz: ClassNode, project: Project): PsiMethod? { @@ -932,3 +987,43 @@ fun MethodInsnNode.fakeResolve(): ClassAndMethodNode { addConstructorToFakeClass(clazz) return ClassAndMethodNode(clazz, method) } + +// Textifier + +fun ClassNode.textify(): String { + val sw = StringWriter() + accept(TraceClassVisitor(PrintWriter(sw))) + return sw.toString().replaceIndent().trimEnd() +} + +fun FieldNode.textify(): String { + val cv = TraceClassVisitor(null) + accept(cv) + val sw = StringWriter() + cv.p.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} + +fun MethodNode.textify(): String { + val cv = TraceClassVisitor(null) + accept(cv) + val sw = StringWriter() + cv.p.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} + +fun AnnotationNode.textify(): String { + val textifier = Textifier() + accept(TraceAnnotationVisitor(textifier)) + val sw = StringWriter() + textifier.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} + +fun AbstractInsnNode.textify(): String { + val mv = TraceMethodVisitor(Textifier()) + accept(mv) + val sw = StringWriter() + mv.p.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} diff --git a/src/main/kotlin/platform/mixin/util/LocalInfo.kt b/src/main/kotlin/platform/mixin/util/LocalInfo.kt index 710e7834e..fc684799d 100644 --- a/src/main/kotlin/platform/mixin/util/LocalInfo.kt +++ b/src/main/kotlin/platform/mixin/util/LocalInfo.kt @@ -24,9 +24,11 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.util.computeStringArray import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.isErasureEquivalentTo import com.intellij.openapi.module.Module import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType +import com.intellij.util.containers.sequenceOfNotNull import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -128,6 +130,27 @@ class LocalInfo( } } + fun matchSourceLocals( + sourceLocals: List + ): Sequence { + if (ordinal != null) { + return sequenceOfNotNull( + sourceLocals.asSequence().filter { it.type.isErasureEquivalentTo(type) }.drop(ordinal).firstOrNull() + ) + } + if (index != null) { + return sequenceOfNotNull(sourceLocals.getOrNull(index)) + } + if (names.isNotEmpty()) { + return sourceLocals.asSequence().filter { it.mixinName in names } + } + + // implicit mode + return sequenceOfNotNull( + sourceLocals.singleOrNull { it.type.isErasureEquivalentTo(type) } + ) + } + companion object { /** * Gets a [LocalInfo] from an annotation which declares the following attributes: diff --git a/src/main/kotlin/platform/mixin/util/LocalVariables.kt b/src/main/kotlin/platform/mixin/util/LocalVariables.kt index 59ee23686..6c5b15441 100644 --- a/src/main/kotlin/platform/mixin/util/LocalVariables.kt +++ b/src/main/kotlin/platform/mixin/util/LocalVariables.kt @@ -119,7 +119,13 @@ object LocalVariables { for (parameter in method.parameterList.parameters) { val mixinName = if (argsOnly) "var$argsIndex" else parameter.name - args += SourceLocalVariable(parameter.name, parameter.type, argsIndex, mixinName = mixinName) + args += SourceLocalVariable( + parameter.name, + parameter.type, + argsIndex, + mixinName = mixinName, + variable = parameter + ) argsIndex++ if (parameter.isDoubleSlot) { argsIndex++ @@ -207,7 +213,12 @@ object LocalVariables { localsHere = localsHere.copyOf(localIndex + 1) } val name = instruction.variable.name ?: return - localsHere[localIndex] = SourceLocalVariable(name, instruction.variable.type, localIndex) + localsHere[localIndex] = SourceLocalVariable( + name, + instruction.variable.type, + localIndex, + variable = instruction.variable + ) if (instruction.variable.isDoubleSlot && localIndex + 1 < localsHere.size) { localsHere[localIndex + 1] = null } @@ -850,11 +861,16 @@ object LocalVariables { } } + /** + * Represents a local variable in source code and its probable relationship to the bytecode. Don't store instances + * of this class. + */ data class SourceLocalVariable( val name: String, val type: PsiType, val index: Int, val mixinName: String = name, + val variable: PsiVariable? = null, val implicitLoadCountBefore: Int = 0, val implicitLoadCountAfter: Int = 0, val implicitStoreCountBefore: Int = 0, diff --git a/src/main/kotlin/platform/mixin/util/MixinConstants.kt b/src/main/kotlin/platform/mixin/util/MixinConstants.kt index 93ae8408d..173fc2050 100644 --- a/src/main/kotlin/platform/mixin/util/MixinConstants.kt +++ b/src/main/kotlin/platform/mixin/util/MixinConstants.kt @@ -84,11 +84,14 @@ object MixinConstants { } object MixinExtras { + const val PACKAGE = "com.llamalad7.mixinextras." const val OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.Operation" const val WRAP_OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation" const val WRAP_METHOD = "com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod" const val LOCAL = "com.llamalad7.mixinextras.sugar.Local" const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref." + const val EXPRESSION = "com.llamalad7.mixinextras.expression.Expression" + const val DEFINITION = "com.llamalad7.mixinextras.expression.Definition" fun PsiType.unwrapLocalRef(): PsiType { if (this !is PsiClassType) { diff --git a/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt b/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt new file mode 100644 index 000000000..bcc1c15d4 --- /dev/null +++ b/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt @@ -0,0 +1,28 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.util + +// See CachedValueStabilityChecker +class UnsafeCachedValueCapture(val value: T) { + override fun hashCode() = 0 + override fun equals(other: Any?) = other is UnsafeCachedValueCapture<*> + override fun toString() = value.toString() +} diff --git a/src/main/kotlin/util/BeforeOrAfter.kt b/src/main/kotlin/util/BeforeOrAfter.kt new file mode 100644 index 000000000..448be3062 --- /dev/null +++ b/src/main/kotlin/util/BeforeOrAfter.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.util + +import com.demonwav.mcdev.asset.MCDevBundle +import java.util.function.Supplier + +enum class BeforeOrAfter(private val myDisplayName: Supplier) { + BEFORE(MCDevBundle.pointer("minecraft.before")), + AFTER(MCDevBundle.pointer("minecraft.after")); + + val displayName get() = myDisplayName.get() + override fun toString() = displayName +} diff --git a/src/main/kotlin/util/MemberReference.kt b/src/main/kotlin/util/MemberReference.kt index 5b5921e73..945746fa5 100644 --- a/src/main/kotlin/util/MemberReference.kt +++ b/src/main/kotlin/util/MemberReference.kt @@ -21,14 +21,12 @@ package com.demonwav.mcdev.util import com.demonwav.mcdev.platform.mixin.reference.MixinSelector -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiClass import com.intellij.psi.PsiField import com.intellij.psi.PsiMethod import java.io.Serializable -import java.lang.reflect.Type +import org.objectweb.asm.Type /** * Represents a reference to a class member (a method or a field). It may @@ -65,6 +63,19 @@ data class MemberReference( override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") } override val displayName = name + val presentableText: String get() = buildString { + if (owner != null) { + append(owner.substringAfterLast('.')) + append('.') + } + append(name) + if (descriptor != null && descriptor.startsWith("(")) { + append('(') + append(Type.getArgumentTypes(descriptor).joinToString { it.className.substringAfterLast('.') }) + append(')') + } + } + override fun canEverMatch(name: String): Boolean { return matchAllNames || this.name == name } @@ -88,13 +99,71 @@ data class MemberReference( (this.descriptor == null || this.descriptor == desc) } - object Deserializer : JsonDeserializer { - override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): MemberReference { - val ref = json.asString - val className = ref.substringBefore('#') - val methodName = ref.substring(className.length + 1, ref.indexOf("(")) - val methodDesc = ref.substring(className.length + methodName.length + 1) - return MemberReference(methodName, methodDesc, className) + companion object { + fun parse(value: String): MemberReference? { + val reference = value.replace(" ", "") + val owner: String? + + var pos = reference.lastIndexOf('.') + if (pos != -1) { + // Everything before the dot is the qualifier/owner + owner = reference.substring(0, pos).replace('/', '.') + } else { + pos = reference.indexOf(';') + if (pos != -1 && reference.startsWith('L')) { + val internalOwner = reference.substring(1, pos) + if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) { + // Invalid: Qualifier should only contain slashes + return null + } + + owner = internalOwner.replace('/', '.') + + // if owner is all there is to the selector, match anything with the owner + if (pos == reference.length - 1) { + return MemberReference("", null, owner, matchAllNames = true, matchAllDescs = true) + } + } else { + // No owner/qualifier specified + pos = -1 + owner = null + } + } + + val descriptor: String? + val name: String + val matchAllNames = reference.getOrNull(pos + 1) == '*' + val matchAllDescs: Boolean + + // Find descriptor separator + val methodDescPos = reference.indexOf('(', pos + 1) + if (methodDescPos != -1) { + // Method descriptor + descriptor = reference.substring(methodDescPos) + name = reference.substring(pos + 1, methodDescPos) + matchAllDescs = false + } else { + val fieldDescPos = reference.indexOf(':', pos + 1) + if (fieldDescPos != -1) { + descriptor = reference.substring(fieldDescPos + 1) + name = reference.substring(pos + 1, fieldDescPos) + matchAllDescs = false + } else { + descriptor = null + matchAllDescs = reference.endsWith('*') + name = if (matchAllDescs) { + reference.substring(pos + 1, reference.lastIndex) + } else { + reference.substring(pos + 1) + } + } + } + + if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { + return null + } + + return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs) } } } diff --git a/src/main/kotlin/util/bytecode-utils.kt b/src/main/kotlin/util/bytecode-utils.kt index 5eab8bbcf..777210c54 100644 --- a/src/main/kotlin/util/bytecode-utils.kt +++ b/src/main/kotlin/util/bytecode-utils.kt @@ -69,7 +69,9 @@ fun getPrimitiveType(internalName: Char): PsiPrimitiveType? { } val PsiType.descriptor - get() = appendDescriptor(StringBuilder()).toString() + get() = erasure().appendDescriptor(StringBuilder()).toString() + +private fun PsiType.erasure() = TypeConversionUtil.erasure(this)!! fun getPrimitiveWrapperClass(internalName: Char, project: Project): PsiClass? { val type = getPrimitiveType(internalName) ?: return null diff --git a/src/main/kotlin/util/psi-utils.kt b/src/main/kotlin/util/psi-utils.kt index af3363228..548ea40ed 100644 --- a/src/main/kotlin/util/psi-utils.kt +++ b/src/main/kotlin/util/psi-utils.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.platform.mcp.McpModule import com.demonwav.mcdev.platform.mcp.McpModuleType import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore @@ -32,10 +33,12 @@ import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.roots.impl.OrderEntryUtil import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolderEx import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.ElementManipulator import com.intellij.psi.ElementManipulators import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiElement @@ -45,12 +48,14 @@ import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiExpression import com.intellij.psi.PsiFile import com.intellij.psi.PsiKeyword +import com.intellij.psi.PsiLanguageInjectionHost import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodReferenceExpression import com.intellij.psi.PsiModifier import com.intellij.psi.PsiModifier.ModifierConstant import com.intellij.psi.PsiModifierList +import com.intellij.psi.PsiNameValuePair import com.intellij.psi.PsiParameter import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiReference @@ -58,14 +63,21 @@ import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiType import com.intellij.psi.ResolveResult import com.intellij.psi.filters.ElementFilter +import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTypesUtil import com.intellij.psi.util.TypeConversionUtil +import com.intellij.psi.util.parentOfType import com.intellij.refactoring.changeSignature.ChangeSignatureUtil import com.intellij.util.IncorrectOperationException import com.siyeh.ig.psiutils.ImportUtils +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write // Parent fun PsiElement.findModule(): Module? = ModuleUtilCore.findModuleForPsiElement(this) @@ -82,6 +94,10 @@ fun PsiElement.findContainingMethod(): PsiMethod? = findParent(resolveReferences fun PsiElement.findContainingModifierList(): PsiModifierList? = findParent(resolveReferences = false) { it is PsiClass } +fun PsiElement.findContainingNameValuePair(): PsiNameValuePair? = findParent(resolveReferences = false) { + it is PsiClass || it is PsiMethod || it is PsiAnnotation +} + private val PsiElement.ancestors: Sequence get() = generateSequence(this) { if (it is PsiFile) null else it.parent } @@ -174,6 +190,18 @@ inline fun PsiElement.childrenOfType(): Collection = inline fun PsiElement.childOfType(): T? = PsiTreeUtil.findChildOfType(this, T::class.java) +/** + * [InjectedLanguageManager.getInjectionHost] returns the first host of a multi-host injection for some reason. + * Use this method as a workaround. + */ +fun PsiElement.findMultiInjectionHost(): PsiLanguageInjectionHost? { + val injectedLanguageManager = InjectedLanguageManager.getInstance(project) + val hostFile = injectedLanguageManager.getInjectionHost(this)?.containingFile ?: return null + val hostOffset = injectedLanguageManager.injectedToHost(this, textRange.startOffset) + val hostElement = hostFile.findElementAt(hostOffset) ?: return null + return hostElement.parentOfType(withSelf = true) +} + fun Sequence.filter(filter: ElementFilter?, context: PsiElement): Sequence { filter ?: return this return filter { filter.isAcceptable(it, context) } @@ -226,6 +254,36 @@ inline fun PsiElement.cached(vararg dependencies: Any, crossinline compute: } } +@PublishedApi +internal val CACHE_LOCKS_KEY = Key.create, ReentrantReadWriteLock>>("mcdev.cacheLock") + +inline fun PsiElement.lockedCached( + key: Key>, + vararg dependencies: Any, + crossinline compute: () -> T, +): T { + val cacheLocks = (this as UserDataHolderEx).putUserDataIfAbsent(CACHE_LOCKS_KEY, ConcurrentHashMap()) + val cacheLock = cacheLocks.computeIfAbsent(key) { ReentrantReadWriteLock() } + + cacheLock.read { + val value = getUserData(key)?.upToDateOrNull + if (value != null) { + return value.get() + } + } + + cacheLock.write { + val value = getUserData(key)?.upToDateOrNull + if (value != null) { + return value.get() + } + + return CachedValuesManager.getCachedValue(this, key) { + CachedValueProvider.Result.create(compute(), *(dependencies.toList() + this).toTypedArray()) + } + } +} + fun LookupElementBuilder.withImportInsertion(toImport: List): LookupElementBuilder = this.withInsertHandler { insertionContext, _ -> toImport.forEach { ImportUtils.addImportIfNeeded(it, insertionContext.file) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 28d579ca5..b15efd6b2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -195,6 +195,7 @@ + @@ -249,6 +250,10 @@ id="Settings.Minecraft" groupId="language" instance="com.demonwav.mcdev.MinecraftConfigurable"/> + + @@ -492,6 +498,7 @@ + @@ -518,6 +525,35 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 2ee0ef24d..7d8526e81 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -260,16 +260,9 @@ translation_sort.title=Select Sort Order translation_sort.order=Sort Order translation_sort.keep_comment=Keep Comment -minecraft.settings.display_name=Minecraft Development -minecraft.settings.title=Minecraft Development Settings minecraft.settings.change_update_channel=Change Plugin Update Channel -minecraft.settings.show_project_platform_icons=Show project platform icons -minecraft.settings.show_event_listener_gutter_icons=Show event listener gutter icons -minecraft.settings.show_chat_color_gutter_icons=Show chat color gutter icons -minecraft.settings.show_chat_color_underlines=Show chat color underlines minecraft.settings.chat_color_underline_style=Chat color underline style: -minecraft.settings.mixin=Mixin -minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line +minecraft.settings.display_name=Minecraft Development minecraft.settings.creator=Creator minecraft.settings.creator.repos=Template Repositories: minecraft.settings.creator.repos.column.name=Name @@ -284,10 +277,59 @@ minecraft.settings.lang_template.project_must_be_selected=You must have selected minecraft.settings.lang_template.comment=You may edit the template used for translation key sorting here.\
Each line may be empty, a comment (with #) or a glob pattern for matching translation keys (like "item.*").\
Note: Empty lines are respected and will be put into the sorting result. +minecraft.settings.mixin.definition_pos_relative_to_expression=@Definition position relative to @Expression +minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line +minecraft.settings.mixin=Mixin +minecraft.settings.project.display_name=Project-Specific Settings +minecraft.settings.show_chat_color_gutter_icons=Show chat color gutter icons +minecraft.settings.show_chat_color_underlines=Show chat color underlines +minecraft.settings.show_event_listener_gutter_icons=Show event listener gutter icons +minecraft.settings.show_project_platform_icons=Show project platform icons +minecraft.settings.title=Minecraft Development Settings minecraft.settings.translation=Translation minecraft.settings.translation.force_json_translation_file=Force JSON translation file (1.13+) minecraft.settings.translation.use_custom_convert_template=Use custom template for convert literal to translation +minecraft.before=Before +minecraft.after=After + +mixinextras.expression.lang.errors.array_access_missing_index=Missing index +mixinextras.expression.lang.errors.array_length_after_empty=Cannot specify array length after an unspecified array length +mixinextras.expression.lang.errors.empty_array_initializer=Array initializer cannot be empty +mixinextras.expression.lang.errors.index_not_expected_in_type=Index not expected in type +mixinextras.expression.lang.errors.instanceof_non_type=Expected type +mixinextras.expression.lang.errors.invalid_number=Invalid number +mixinextras.expression.lang.errors.missing_array_length=Array construction must contain a length +mixinextras.expression.lang.errors.new_array_dim_expr_with_initializer=Cannot use initializer for array with specified length +mixinextras.expression.lang.errors.new_no_constructor_args_or_array=Expected constructor arguments or array creation +mixinextras.expression.lang.errors.unresolved_symbol=Unresolved symbol +mixinextras.expression.lang.errors.unused_definition=Unused definition +mixinextras.expression.lang.errors.unused_symbol.fix=Remove definition + +mixinextras.expression.lang.display_name=MixinExtras Expressions +mixinextras.expression.lang.highlighting.bad_char.display_name=Bad character +mixinextras.expression.lang.highlighting.braces.display_name=Braces +mixinextras.expression.lang.highlighting.brackets.display_name=Brackets +mixinextras.expression.lang.highlighting.call_identifier.display_name=Identifier//Method call +mixinextras.expression.lang.highlighting.capture.display_name=Capture +mixinextras.expression.lang.highlighting.class_name_identifier.display_name=Identifier//Class name +mixinextras.expression.lang.highlighting.comma.display_name=Comma +mixinextras.expression.lang.highlighting.declaration_identifier.display_name=Identifier//Declaration +mixinextras.expression.lang.highlighting.dot.display_name=Dot +mixinextras.expression.lang.highlighting.identifier.display_name=Identifier +mixinextras.expression.lang.highlighting.keyword.display_name=Keyword +mixinextras.expression.lang.highlighting.member_name_identifier.display_name=Identifier//Member name +mixinextras.expression.lang.highlighting.method_reference.display_name=Method reference +mixinextras.expression.lang.highlighting.number.display_name=Number +mixinextras.expression.lang.highlighting.operator.display_name=Operator +mixinextras.expression.lang.highlighting.parens.display_name=Parentheses +mixinextras.expression.lang.highlighting.primitive_type_identifier.display_name=Identifier//Primitive type +mixinextras.expression.lang.highlighting.string.display_name=String +mixinextras.expression.lang.highlighting.string_escape.display_name=String escape +mixinextras.expression.lang.highlighting.type_declaration_identifier.display_name=Identifier//Type declaration +mixinextras.expression.lang.highlighting.variable_identifier.display_name=Identifier//Variable +mixinextras.expression.lang.highlighting.wildcard.display_name=Wildcard + template.provider.builtin.label=Built In template.provider.remote.label=Remote template.provider.local.label=Local diff --git a/src/test/kotlin/platform/mixin/BaseMixinTest.kt b/src/test/kotlin/platform/mixin/BaseMixinTest.kt index 4b9dbd0ee..caac9bc5d 100644 --- a/src/test/kotlin/platform/mixin/BaseMixinTest.kt +++ b/src/test/kotlin/platform/mixin/BaseMixinTest.kt @@ -34,20 +34,23 @@ import org.junit.jupiter.api.BeforeEach abstract class BaseMixinTest : BaseMinecraftTest(PlatformType.MIXIN) { private var mixinLibrary: Library? = null + private var mixinExtrasLibrary: Library? = null private var testDataLibrary: Library? = null @BeforeEach fun initMixin() { runWriteTask { mixinLibrary = createLibrary(project, "mixin") + mixinExtrasLibrary = createLibrary(project, "mixinextras-common") testDataLibrary = createLibrary(project, "mixin-test-data") } ModuleRootModificationUtil.updateModel(module) { model -> model.addLibraryEntry(mixinLibrary ?: throw IllegalStateException("Mixin library not created")) + model.addLibraryEntry(mixinExtrasLibrary ?: throw IllegalStateException("MixinExtras library not created")) model.addLibraryEntry(testDataLibrary ?: throw IllegalStateException("Test data library not created")) val orderEntries = model.orderEntries - orderEntries.rotate(2) + orderEntries.rotate(3) model.rearrangeOrderEntries(orderEntries) } } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index ae88d95a5..dfa58058f 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -33,7 +33,7 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { private fun doTest(testName: String) { fixture.enableInspections(InvalidInjectorMethodSignatureInspection::class) - testInspectionFix(fixture, "invalidInjectorMethodSignature/$testName", "Fix method parameters") + testInspectionFix(fixture, "invalidInjectorMethodSignature/$testName", "Fix method signature") } @Test diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 769b3894c..ae97fb26e 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -98,7 +98,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { } @Inject(method = "(Lcom/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureInspection/MixedInOuter;Ljava/lang/String;)V", at = @At("RETURN")) - private void injectCtor(String string, CallbackInfo ci) { + private void injectCtor(String string, CallbackInfo ci) { } } """, @@ -122,7 +122,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { public class TestMixin { @Inject(method = "()V", at = @At("RETURN")) - private void injectCtorWrong(MixedInOuter outer, CallbackInfo ci) { + private void injectCtorWrong(MixedInOuter outer, CallbackInfo ci) { } @Inject(method = "", at = @At("RETURN")) @@ -130,7 +130,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { } @Inject(method = "(Ljava/lang/String;)V", at = @At("RETURN")) - private void injectCtor(MixedInOuter outer, String string, CallbackInfo ci) { + private void injectCtor(MixedInOuter outer, String string, CallbackInfo ci) { } @Inject(method = "(Ljava/lang/String;)V", at = @At("RETURN")) diff --git a/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt b/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt new file mode 100644 index 000000000..5c12d882d --- /dev/null +++ b/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt @@ -0,0 +1,645 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.expression + +import com.demonwav.mcdev.MinecraftProjectSettings +import com.demonwav.mcdev.framework.EdtInterceptor +import com.demonwav.mcdev.platform.mixin.BaseMixinTest +import com.demonwav.mcdev.util.BeforeOrAfter +import com.intellij.codeInsight.lookup.impl.LookupImpl +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(EdtInterceptor::class) +@DisplayName("MixinExtras expression completion test") +class MEExpressionCompletionTest : BaseMixinTest() { + private fun assertLookupAppears( + lookupString: String, + @Language("JAVA") code: String, + shouldAppear: Boolean = true + ) { + buildProject { + dir("test") { + java("MEExpressionCompletionTest.java", code) + } + } + + fixture.completeBasic() + + val lookups = fixture.lookupElementStrings + if (lookups != null) { + if (shouldAppear) { + assertTrue(lookupString in lookups) + } else { + assertFalse(lookupString in lookups) + } + } else { + if (shouldAppear) { + assertEquals(lookupString, fixture.elementAtCaret.text) + } else { + assertNotEquals(lookupString, fixture.elementAtCaret.text) + } + } + } + + private fun doBeforeAfterTest( + lookupString: String, + @Language("JAVA") code: String, + @Language("JAVA") expectedAfter: String? + ) { + buildProject { + dir("test") { + java("MEExpressionCompletionTest.java", code) + } + } + + MinecraftProjectSettings.getInstance(fixture.project).definitionPosRelativeToExpression = BeforeOrAfter.BEFORE + + val possibleItems = fixture.completeBasic() + if (possibleItems != null) { + val itemToComplete = possibleItems.firstOrNull { it.lookupString == lookupString } + if (expectedAfter != null) { + assertNotNull(itemToComplete, "Expected a completion matching \"$lookupString\"") + (fixture.lookup as LookupImpl).finishLookup('\n', itemToComplete) + } else { + assertNull(itemToComplete, "Expected no completions matching \"$lookupString\"") + return + } + } else if (expectedAfter == null) { + fail("Expected no completions matching \"$lookupString\"") + return + } + + fixture.checkResult(expectedAfter) + } + + @Test + @DisplayName("Local Variable Implicit Completion Test") + fun localVariableImplicitCompletionTest() { + doBeforeAfterTest( + "one", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import com.llamalad7.mixinextras.sugar.Local; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "one", local = @Local(type = int.class)) + @Expression("one") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Local Variable Ordinal Completion Test") + fun localVariableOrdinalCompletionTest() { + doBeforeAfterTest( + "local1", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import com.llamalad7.mixinextras.sugar.Local; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "local1", local = @Local(type = String.class, ordinal = 0)) + @Expression("local1") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Local Variable Inaccessible Type Completion Test") + fun localVariableInaccessibleTypeCompletionTest() { + doBeforeAfterTest( + "varOfInaccessibleType", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V") + @Expression("acceptInaccessibleType()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import com.llamalad7.mixinextras.sugar.Local; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V") + @Definition(id = "varOfInaccessibleType", local = @Local(ordinal = 0)) + @Expression("acceptInaccessibleType(varOfInaccessibleType)") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Field Completion Test") + fun fieldCompletionTest() { + doBeforeAfterTest( + "out", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "out", field = "Ljava/lang/System;out:Ljava/io/PrintStream;") + @Expression("out") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Method Completion Test") + fun methodCompletionTest() { + doBeforeAfterTest( + "acceptInaccessibleType", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V") + @Expression("acceptInaccessibleType()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Method No-Arg Completion Test") + fun methodNoArgCompletionTest() { + doBeforeAfterTest( + "noArgMethod", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "noArgMethod", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;noArgMethod()V") + @Expression("noArgMethod()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Type Completion Test") + fun typeCompletionTest() { + doBeforeAfterTest( + "ArrayList", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("new ") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + import java.util.ArrayList; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "ArrayList", type = ArrayList.class) + @Expression("new ArrayList()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Inaccessible Type Completion Test") + fun inaccessibleTypeCompletionTest() { + doBeforeAfterTest( + "InaccessibleType", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("new ") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + null, + ) + } + + @Test + @DisplayName("Array Creation Completion Test") + fun arrayCreationCompletionTest() { + doBeforeAfterTest( + "String", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("new ") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "String", type = String.class) + @Expression("new String[]") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("LHS Of Complete Assignment Test") + fun lhsOfCompleteAssignmentTest() { + assertLookupAppears( + "local1", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression(" = 'Hello'") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Cast Test") + fun castTest() { + assertLookupAppears( + "Integer", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("()") + @Inject(method = "getStingerCount", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Member Function Test") + fun memberFunctionTest() { + assertLookupAppears( + "get", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "Integer", type = Integer.class) + @Definition(id = "synchedData", field = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;synchedData:Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}SynchedDataManager;") + @Expression("(Integer) this.synchedData.") + @Inject(method = "getStingerCount", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Array Length Test") + fun arrayLengthTest() { + assertLookupAppears( + "one", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "String", type = String.class) + @Expression("new String[]") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Array Element Test") + fun arrayElementTest() { + assertLookupAppears( + "local2", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "String", type = String.class) + @Expression("new String[]{?, }") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Static Method Reference Test") + fun staticMethodReferenceTest() { + assertLookupAppears( + "staticMapper", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("::") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Non Static Method Reference Test") + fun nonStaticMethodReferenceTest() { + assertLookupAppears( + "nonStaticMapper", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("this::") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Constructor Method Reference Test") + fun constructorMethodReferenceTest() { + assertLookupAppears( + "ConstructedByMethodReference", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } +} From 858bbc30bd6c85fb96b818c73837e7d68f7f56e6 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 12:39:02 +0200 Subject: [PATCH 27/37] Track v1 version of the templates repo --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index ed1c5f036..3bcb1c8f6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "templates"] path = templates - branch = main + branch = v1 url = https://github.com/minecraft-dev/templates From 6fa2235d603088a6ba4e82782394ab8c3f4dbb9a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 12:50:26 +0200 Subject: [PATCH 28/37] Comment out test settings --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 450c44ad1..7eb7103b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -425,8 +425,8 @@ tasks.runIde { systemProperty("idea.debug.mode", "true") } // Set these properties to test different languages - systemProperty("user.language", "fr") - systemProperty("user.country", "FR") + // systemProperty("user.language", "fr") + // systemProperty("user.country", "FR") } tasks.buildSearchableOptions { From 84f01917f34ebeec08e951e25e6d53482ad4110e Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 11 Jul 2024 15:50:56 +0200 Subject: [PATCH 29/37] Set up changelogs and first draft for 1.8.0 --- build.gradle.kts | 14 ++++++++++++++ changelog.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 changelog.md diff --git a/build.gradle.kts b/build.gradle.kts index 7eb7103b6..da7da143a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ import org.cadixdev.gradle.licenser.header.HeaderStyle import org.cadixdev.gradle.licenser.tasks.LicenseUpdate import org.gradle.internal.jvm.Jvm +import org.jetbrains.changelog.Changelog import org.jetbrains.gradle.ext.settings import org.jetbrains.gradle.ext.taskTriggers import org.jetbrains.intellij.tasks.PrepareSandboxTask @@ -37,6 +38,7 @@ plugins { id("org.jetbrains.intellij") version "1.17.2" id("org.cadixdev.licenser") id("org.jlleitschuh.gradle.ktlint") version "10.3.0" + id("org.jetbrains.changelog") version "2.2.0" } val ideaVersionName: String by project @@ -191,6 +193,12 @@ configurations.compileClasspath { attributes.attribute(filtered, true) } +changelog { + groups.empty() + path = "changelog.md" + repositoryUrl = "https://github.com/minecraft-dev/MinecraftDev" +} + intellij { // IntelliJ IDEA dependency version.set(providers.gradleProperty("ideaVersion")) @@ -217,6 +225,12 @@ intellij { sandboxDir.set(layout.projectDirectory.dir(".sandbox").toString()) } +tasks.patchPluginXml { + val changelog = project.changelog + val item = changelog.getOrNull(version.toString()) ?: changelog.getUnreleased() + changeNotes = changelog.renderItem(item.withHeader(false).withEmptySections(false), Changelog.OutputType.HTML) +} + tasks.publishPlugin { // Build numbers are used for properties["buildNumber"]?.let { buildNumber -> diff --git a/changelog.md b/changelog.md new file mode 100644 index 000000000..c00d1b418 --- /dev/null +++ b/changelog.md @@ -0,0 +1,42 @@ +# Minecraft Development for IntelliJ + +## [Unreleased] + +This release contains two major features: +- Support for MixinExtras expressions ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) +- A rewritten project creator ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) + +The new project creator is very similar to the previous one but has a few advantages: +- The templates are now stored on a separate repository and updated the first time you open the creator. This allows us to release template updates independently of plugin releases. +- You can create your own custom templates in their own repositories, which can be: + - flat directories + - local ZIP archives + - remote ZIP archives (like the built-in templates) +- Kotlin templates were added to all platforms except Forge and Architectury (couldn't get the Forge one to work, will look into it later) +- Fabric now has a split sources option +- Some niche options like the plugins dependencies fields were removed as their use was quite limited +- Remembered field values won't be ported over to the new creator, so make sure to configure your Group ID under Build System Properties! +- The old creator will be kept for a few months to give us the time to fix the new creator, please report any issues on the [issue tracker](https://github.com/minecraft-dev/MinecraftDev/issues) + +--- + +### Added + +- Initial support for NeoForge's ModDevGradle +- Option to force json translation and configurable default i18n call ([#2292](https://github.com/minecraft-dev/MinecraftDev/pull/2292)) +- Minecraft version detection for Loom-based projects +- Other JVM languages support for translation references, inspections and code folding +- Repo-based project creator templates ([#2304](https://github.com/minecraft-dev/MinecraftDev/pull/2304)) +- Support for MixinExtras expressions ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) + +### Changed + +- [#2296](https://github.com/minecraft-dev/MinecraftDev/issues/2296) Support entry point container objects in fabric.mod.json +- [#2325](https://github.com/minecraft-dev/MinecraftDev/issues/2325) Make lang annotator fixes bulk compatible +- Migrated the remaining legacy forms to the Kotlin UI DSL + +### Fixed + +- [#2316](https://github.com/minecraft-dev/MinecraftDev/issues/2316) Sponge's injection inspection isn't aware of the Configurate 4 classes ([#2317](https://github.com/minecraft-dev/MinecraftDev/pull/2317)) +- [#2310](https://github.com/minecraft-dev/MinecraftDev/issues/2310) Translations aren't detected for enum constructors +- [#2260](https://github.com/minecraft-dev/MinecraftDev/issues/2260) Vararg return type expected in a @ModifyArg method From 71d6934d4a39e7a1c07912491f6fd0694681fa38 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 15:56:00 +0200 Subject: [PATCH 30/37] Fix link to the creator rewrite --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index c00d1b418..3d1b6878b 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,7 @@ This release contains two major features: - Support for MixinExtras expressions ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) -- A rewritten project creator ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) +- A rewritten project creator ([#2304](https://github.com/minecraft-dev/MinecraftDev/pull/2304)) The new project creator is very similar to the previous one but has a few advantages: - The templates are now stored on a separate repository and updated the first time you open the creator. This allows us to release template updates independently of plugin releases. From 3f6f235cc39849350f81c73a31eaf26227deb1a5 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 16:42:20 +0200 Subject: [PATCH 31/37] Render the whole changelog This will do for now, so users have access to more context when checking future patches Also remove the repositoryUrl configuration because it causes the generation of invalid links to GitHub tags --- build.gradle.kts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index da7da143a..bc5a8901e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -194,9 +194,9 @@ configurations.compileClasspath { } changelog { + version = coreVersion groups.empty() path = "changelog.md" - repositoryUrl = "https://github.com/minecraft-dev/MinecraftDev" } intellij { @@ -227,8 +227,7 @@ intellij { tasks.patchPluginXml { val changelog = project.changelog - val item = changelog.getOrNull(version.toString()) ?: changelog.getUnreleased() - changeNotes = changelog.renderItem(item.withHeader(false).withEmptySections(false), Changelog.OutputType.HTML) + changeNotes = changelog.render(Changelog.OutputType.HTML) } tasks.publishPlugin { From 1b24c8dc91fdeb7cbf34a9c2dd4337ff89e2b21a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 16:42:54 +0200 Subject: [PATCH 32/37] Small changelog tweak for clarity --- changelog.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 3d1b6878b..48b69e895 100644 --- a/changelog.md +++ b/changelog.md @@ -1,11 +1,11 @@ # Minecraft Development for IntelliJ -## [Unreleased] - This release contains two major features: - Support for MixinExtras expressions ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) - A rewritten project creator ([#2304](https://github.com/minecraft-dev/MinecraftDev/pull/2304)) +### About the new project creator + The new project creator is very similar to the previous one but has a few advantages: - The templates are now stored on a separate repository and updated the first time you open the creator. This allows us to release template updates independently of plugin releases. - You can create your own custom templates in their own repositories, which can be: @@ -18,8 +18,6 @@ The new project creator is very similar to the previous one but has a few advant - Remembered field values won't be ported over to the new creator, so make sure to configure your Group ID under Build System Properties! - The old creator will be kept for a few months to give us the time to fix the new creator, please report any issues on the [issue tracker](https://github.com/minecraft-dev/MinecraftDev/issues) ---- - ### Added - Initial support for NeoForge's ModDevGradle From 6378fa212dc45676cb9f4624608025f2fbf5faab Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 16:43:36 +0200 Subject: [PATCH 33/37] Explicitly exclude the .git directory just to be safe --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index bc5a8901e..cce2ac752 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -426,6 +426,7 @@ tasks.withType { into("Minecraft Development/lib/resources") } from("templates") { + exclude(".git") into("Minecraft Development/lib/resources/builtin-templates") } } From 40c051478534cc81486e4d1042bc9280c71f2e38 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 16:45:38 +0200 Subject: [PATCH 34/37] Version 1.8.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9bebb7a1c..5d3d680ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ kotlin.code.style=official ideaVersion = 2023.2.2 ideaVersionName = 2023.2.2 -coreVersion = 1.7.6 +coreVersion = 1.8.0 downloadIdeaSources = true pluginTomlVersion = 232.8660.88 From 246c1d5f65e7042b1d42397c8a25d41b9bddf87f Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 13 Jul 2024 18:12:52 +0200 Subject: [PATCH 35/37] Pass project to finalizers --- src/main/kotlin/creator/custom/CustomPlatformStep.kt | 2 +- .../creator/custom/finalizers/CreatorFinalizer.kt | 11 +++++++++-- .../creator/custom/finalizers/GitAddAllFinalizer.kt | 8 +++++++- .../custom/finalizers/ImportGradleProjectFinalizer.kt | 9 +++++++-- .../custom/finalizers/ImportMavenProjectFinalizer.kt | 9 +++++++-- .../custom/finalizers/RunGradleTasksFinalizer.kt | 9 +++++++-- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index 8c54b0bf9..2052cbae9 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -515,7 +515,7 @@ class CustomPlatformStep( val finalizers = selectedTemplate.descriptor.finalizers if (!finalizers.isNullOrEmpty()) { - CreatorFinalizer.executeAll(context, finalizers, templateProperties) + CreatorFinalizer.executeAll(context, project, finalizers, templateProperties) } } } diff --git a/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt index a65e225b7..4442f33f7 100644 --- a/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt +++ b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt @@ -28,6 +28,7 @@ import com.intellij.openapi.diagnostic.ControlFlowException import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.project.Project import com.intellij.openapi.util.KeyedExtensionCollector import com.intellij.serviceContainer.BaseKeyedLazyInstance import com.intellij.util.KeyedLazyInstance @@ -37,7 +38,12 @@ interface CreatorFinalizer { fun validate(reporter: TemplateValidationReporter, properties: Map) = Unit - fun execute(context: WizardContext, properties: Map, templateProperties: Map) + fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) companion object { private val EP_NAME = @@ -79,6 +85,7 @@ interface CreatorFinalizer { fun executeAll( context: WizardContext, + project: Project, finalizers: List>, templateProperties: Map ) { @@ -93,7 +100,7 @@ interface CreatorFinalizer { val finalizer = COLLECTOR.findSingle(type)!! try { - finalizer.execute(context, properties, templateProperties) + finalizer.execute(context, project, properties, templateProperties) } catch (t: Throwable) { if (t is ControlFlowException) { throw t diff --git a/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt index ea099c9f8..e2ebd6863 100644 --- a/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt +++ b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt @@ -23,10 +23,16 @@ package com.demonwav.mcdev.creator.custom.finalizers import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.util.ExecUtil import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.project.Project class GitAddAllFinalizer : CreatorFinalizer { - override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { ExecUtil.execAndGetOutput(GeneralCommandLine("git", "add", ".").withWorkDirectory(context.projectFileDirectory)) } } diff --git a/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt index 7385d945e..2115dc3b8 100644 --- a/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt +++ b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt @@ -22,13 +22,18 @@ package com.demonwav.mcdev.creator.custom.finalizers import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project import org.jetbrains.plugins.gradle.service.project.open.canLinkAndRefreshGradleProject import org.jetbrains.plugins.gradle.service.project.open.linkAndRefreshGradleProject class ImportGradleProjectFinalizer : CreatorFinalizer { - override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { - val project = context.project!! + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { val projectDir = context.projectFileDirectory val canLink = canLinkAndRefreshGradleProject(projectDir, project, showValidationDialog = false) thisLogger().info("canLink = $canLink projectDir = $projectDir") diff --git a/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt index fb0652c57..3e1e71a0f 100644 --- a/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt +++ b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.creator.custom.finalizers import com.demonwav.mcdev.util.invokeAndWait import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import java.nio.file.Path import java.util.concurrent.TimeUnit @@ -30,8 +31,12 @@ import org.jetbrains.idea.maven.project.importing.MavenImportingManager class ImportMavenProjectFinalizer : CreatorFinalizer { - override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { - val project = context.project!! + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { val projectDir = context.projectFileDirectory val pomFile = VfsUtil.findFile(Path.of(projectDir).resolve("pom.xml"), true) diff --git a/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt index 9d919d1f9..210920426 100644 --- a/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt +++ b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.util.runGradleTaskAndWait import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project class RunGradleTasksFinalizer : CreatorFinalizer { @@ -38,10 +39,14 @@ class RunGradleTasksFinalizer : CreatorFinalizer { } } - override fun execute(context: WizardContext, properties: Map, templateProperties: Map) { + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { @Suppress("UNCHECKED_CAST") val tasks = properties["tasks"] as List - val project = context.project!! val projectDir = context.projectDirectory thisLogger().info("tasks = $tasks projectDir = $projectDir") From 11fceec01f5bdf96cb5570eb55433f4f9c063b98 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 14 Jul 2024 11:54:42 +0200 Subject: [PATCH 36/37] Change how creator finalizers are run Should help with some weird deadlocks in future IDE versions --- .../creator/custom/CustomPlatformStep.kt | 21 ++++--- .../kotlin/creator/custom/TemplateService.kt | 60 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 3 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/creator/custom/TemplateService.kt diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index 2052cbae9..6d54cab04 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -38,6 +38,7 @@ import com.intellij.ide.wizard.AbstractNewProjectWizardStep import com.intellij.ide.wizard.GitNewProjectWizardData import com.intellij.ide.wizard.NewProjectWizardBaseData import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.application.WriteAction import com.intellij.openapi.diagnostic.Attachment import com.intellij.openapi.diagnostic.ControlFlowException import com.intellij.openapi.diagnostic.getOrLogException @@ -502,22 +503,26 @@ class CustomPlatformStep( } } - application.executeOnPooledThread { - application.invokeLater({ - application.runWriteAction { - LocalFileSystem.getInstance().refresh(false) - // Apparently a module root is required for the reformat to work - setupTempRootModule(project, projectPath) - } + val finalizeAction = { + WriteAction.runAndWait { + LocalFileSystem.getInstance().refresh(false) + // Apparently a module root is required for the reformat to work + setupTempRootModule(project, projectPath) + reformatFiles(project, generatedFiles) openFilesInEditor(project, generatedFiles) - }, project.disposed) + } val finalizers = selectedTemplate.descriptor.finalizers if (!finalizers.isNullOrEmpty()) { CreatorFinalizer.executeAll(context, project, finalizers, templateProperties) } } + if (context.isCreatingNewProject) { + TemplateService.instance.registerFinalizerAction(project, finalizeAction) + } else { + application.executeOnPooledThread { finalizeAction() } + } } private fun setupTempRootModule(project: Project, projectPath: Path) { diff --git a/src/main/kotlin/creator/custom/TemplateService.kt b/src/main/kotlin/creator/custom/TemplateService.kt new file mode 100644 index 000000000..28c9094de --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateService.kt @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.creator.custom + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.util.application + +@Service +class TemplateService { + + private val pendingActions: MutableMap Unit> = mutableMapOf() + + fun registerFinalizerAction(project: Project, action: suspend () -> Unit) { + if (pendingActions.containsKey(project.name)) { + thisLogger().error("More than one finalizer action registered for project $project") + return + } + + pendingActions[project.locationHash] = action + } + + suspend fun executeFinalizer(project: Project) { + pendingActions.remove(project.locationHash)?.invoke() + } + + companion object { + + val instance: TemplateService + get() = application.service() + } +} + +class TemplateProjectFinalizerActivity : ProjectActivity { + + override suspend fun execute(project: Project) { + TemplateService.instance.executeFinalizer(project) + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b15efd6b2..362a1fab0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -220,6 +220,7 @@ + From 9a4867a34326b74de1760946daf6f657f779ec83 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 14 Jul 2024 12:44:14 +0200 Subject: [PATCH 37/37] 2023.3 creator fixes --- .../creator/custom/CustomPlatformStep.kt | 14 ++++++----- .../finalizers/ImportMavenProjectFinalizer.kt | 25 ++++++------------- .../custom/types/SimpleCreatorProperty.kt | 2 +- .../MEExpressionCompletionContributor.kt | 13 +++++----- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index 6d54cab04..682e4256d 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -124,7 +124,7 @@ class CustomPlatformStep( lateinit var templatePropertyPlaceholder: Placeholder builder.row(MCDevBundle("creator.ui.custom.repos.label")) { - segmentedButton(templateRepos) { it.name } + segmentedButton(templateRepos) { text = it.name } .bind(templateRepoProperty) }.visible(templateRepos.size > 1) @@ -153,7 +153,7 @@ class CustomPlatformStep( builder.row(MCDevBundle("creator.ui.custom.groups.label")) { availableGroupsSegmentedButton = - segmentedButton(emptyList(), String::toString) + segmentedButton(emptyList()) { text = it } .bind(selectedGroupProperty) }.visibleIf( availableGroupsProperty.transform { it.size > 1 } @@ -161,8 +161,10 @@ class CustomPlatformStep( builder.row(MCDevBundle("creator.ui.custom.templates.label")) { availableTemplatesSegmentedButton = - segmentedButton(emptyList(), LoadedTemplate::label, LoadedTemplate::tooltip) - .bind(selectedTemplateProperty) + segmentedButton(emptyList()) { template: LoadedTemplate -> + text = template.label + toolTipText = template.tooltip + }.bind(selectedTemplateProperty) .validation { addApplyRule("", condition = ::hasTemplateErrors) } @@ -172,7 +174,7 @@ class CustomPlatformStep( availableTemplatesProperty.afterChange { newTemplates -> val groups = newTemplates.mapTo(linkedSetOf()) { it.descriptor.translatedGroup } - availableGroupsSegmentedButton.items(groups) + availableGroupsSegmentedButton.items = groups // availableGroupsSegmentedButton.visible(groups.size > 1) availableGroups = groups selectedGroup = groups.firstOrNull() ?: "empty" @@ -180,7 +182,7 @@ class CustomPlatformStep( selectedGroupProperty.afterChange { group -> val templates = availableTemplates.filter { it.descriptor.translatedGroup == group } - availableTemplatesSegmentedButton.items(templates) + availableTemplatesSegmentedButton.items = templates // Force visiblity because the component might become hidden and not show up again // when the segmented button switches between dropdown and buttons availableTemplatesSegmentedButton.visible(true) diff --git a/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt index 3e1e71a0f..38aefd975 100644 --- a/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt +++ b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt @@ -20,14 +20,14 @@ package com.demonwav.mcdev.creator.custom.finalizers -import com.demonwav.mcdev.util.invokeAndWait import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import java.nio.file.Path -import java.util.concurrent.TimeUnit -import org.jetbrains.idea.maven.project.importing.MavenImportingManager +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.buildtool.MavenImportSpec +import org.jetbrains.idea.maven.project.MavenProjectsManager class ImportMavenProjectFinalizer : CreatorFinalizer { @@ -38,25 +38,16 @@ class ImportMavenProjectFinalizer : CreatorFinalizer { templateProperties: Map ) { val projectDir = context.projectFileDirectory - val pomFile = VfsUtil.findFile(Path.of(projectDir).resolve("pom.xml"), true) ?: return - thisLogger().info("Invoking import on EDT pomFile = ${pomFile.path}") - val promise = invokeAndWait { - if (project.isDisposed || !project.isInitialized) { - return@invokeAndWait null - } - MavenImportingManager.getInstance(project).linkAndImportFile(pomFile) - } - - if (promise == null) { - thisLogger().info("Could not start import") - return + thisLogger().info("Invoking import on EDT pomFile = ${pomFile.path}") + val projectsManager = MavenProjectsManager.getInstance(project) + projectsManager.addManagedFiles(listOf(pomFile)) + runBlocking { + projectsManager.updateAllMavenProjects(MavenImportSpec(true, true, false)) } - thisLogger().info("Waiting for import to finish") - promise.finishPromise.blockingGet(Int.MAX_VALUE, TimeUnit.SECONDS) thisLogger().info("Import finished") } } diff --git a/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt index 7a735d7fb..eee7e0ee1 100644 --- a/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt @@ -100,7 +100,7 @@ abstract class SimpleCreatorProperty( it.validationOnApply(validation) } } else { - segmentedButton(options.keys) { options[it] ?: it.toString() } + segmentedButton(options.keys) { text = options[it] ?: it.toString() } .bind(graphProperty) .enabled(descriptor.editable != false) .maxButtonsCount(4) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt index 339634a39..762a7c22c 100644 --- a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.platform.mixin.expression import com.intellij.codeInsight.TailType +import com.intellij.codeInsight.TailTypes import com.intellij.codeInsight.completion.BasicExpressionCompletionContributor import com.intellij.codeInsight.completion.CompletionContributor import com.intellij.codeInsight.completion.CompletionParameters @@ -38,8 +39,8 @@ class MEExpressionCompletionContributor : CompletionContributor() { CompletionType.BASIC, MEExpressionCompletionUtil.STATEMENT_KEYWORD_PLACE, KeywordCompletionProvider( - Keyword("return", TailType.INSERT_SPACE), - Keyword("throw", TailType.INSERT_SPACE), + Keyword("return", TailTypes.insertSpaceType()), + Keyword("throw", TailTypes.insertSpaceType()), ) ) extend( @@ -51,7 +52,7 @@ class MEExpressionCompletionContributor : CompletionContributor() { Keyword("true"), Keyword("false"), Keyword("null"), - Keyword("new", TailType.INSERT_SPACE), + Keyword("new", TailTypes.insertSpaceType()), ) ) extend( @@ -65,7 +66,7 @@ class MEExpressionCompletionContributor : CompletionContributor() { CompletionType.BASIC, MEExpressionCompletionUtil.INSTANCEOF_PLACE, KeywordCompletionProvider( - Keyword("instanceof", TailType.INSERT_SPACE) + Keyword("instanceof", TailTypes.insertSpaceType()) ) ) extend( @@ -123,7 +124,7 @@ class MEExpressionCompletionContributor : CompletionContributor() { keywords.map { keyword -> var lookupItem = BasicExpressionCompletionContributor.createKeywordLookupItem(parameters.position, keyword.name) - if (keyword.tailType != TailType.NONE) { + if (keyword.tailType != TailTypes.noneType()) { lookupItem = object : TailTypeDecorator(lookupItem) { override fun computeTailType(context: InsertionContext?) = keyword.tailType } @@ -134,5 +135,5 @@ class MEExpressionCompletionContributor : CompletionContributor() { } } - private class Keyword(val name: String, val tailType: TailType = TailType.NONE) + private class Keyword(val name: String, val tailType: TailType = TailTypes.noneType()) }