diff --git a/src/main/kotlin/com/demonwav/mcdev/insight/ColorUtil.kt b/src/main/kotlin/com/demonwav/mcdev/insight/ColorUtil.kt index 2f9204df8..d423fbe5c 100644 --- a/src/main/kotlin/com/demonwav/mcdev/insight/ColorUtil.kt +++ b/src/main/kotlin/com/demonwav/mcdev/insight/ColorUtil.kt @@ -62,12 +62,13 @@ fun PsiElement.setColor(color: String) { } } -fun PsiLiteralExpression.setColor(value: Int) { +fun PsiLiteralExpression.setColor(value: Int, hasAlpha: Boolean = false) { this.containingFile.runWriteAction { val node = this.node + val padLength = if (hasAlpha) 8 else 6 val literalExpression = JavaPsiFacade.getElementFactory(this.project) - .createExpressionFromText("0x" + Integer.toHexString(value).toUpperCase(), null) as PsiLiteralExpression + .createExpressionFromText("0x" + Integer.toHexString(value).toUpperCase().padStart(padLength, '0'), null) as PsiLiteralExpression node.psi.replace(literalExpression) } diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/McpModule.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/McpModule.kt index 722c9b6a8..4f5cd51fb 100644 --- a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/McpModule.kt +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/McpModule.kt @@ -14,6 +14,7 @@ import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.i18n.I18nFileListener import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.mcp.color.McpColorMethods import com.demonwav.mcdev.platform.mcp.srg.SrgManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager @@ -31,6 +32,7 @@ class McpModule(facet: MinecraftFacet) : AbstractModule(facet) { var srgManager: SrgManager? = null private set + val colorMethods = McpColorMethods[settings.state.minecraftVersion ?: "1.12"] override fun init() { val files = getSettings().mappingFiles diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorAnnotator.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorAnnotator.kt new file mode 100644 index 000000000..ccc9fc305 --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorAnnotator.kt @@ -0,0 +1,30 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.insight.ColorAnnotator +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +import com.intellij.psi.PsiElement + +class McpColorAnnotator : Annotator { + + override fun annotate(element: PsiElement, holder: AnnotationHolder) { + if (!MinecraftSettings.instance.isShowChatColorUnderlines) { + return + } + + for (call in element.findColors()) { + ColorAnnotator.setColorAnnotator(call.arg, element, holder) + } + } +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorLineMarkerProvider.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorLineMarkerProvider.kt new file mode 100644 index 000000000..03141d619 --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorLineMarkerProvider.kt @@ -0,0 +1,79 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color + +import com.intellij.codeHighlighting.Pass +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.codeInsight.daemon.MergeableLineMarkerInfo +import com.intellij.codeInsight.daemon.NavigateAction +import com.intellij.icons.AllIcons +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiUtilBase +import com.intellij.ui.ColorChooser +import com.intellij.util.Function +import com.intellij.util.ui.ColorIcon +import com.intellij.util.ui.TwoColorsIcon +import java.awt.Color +import javax.swing.Icon + +class McpColorLineMarkerProvider : LineMarkerProvider { + override fun getLineMarkerInfo(element: PsiElement) = null + + override fun collectSlowLineMarkers(elements: List, result: MutableCollection>) { + for (element in elements) { + val calls = element.findColors() + + for (call in calls) { + val info = McpColorInfo(element, call) + NavigateAction.setNavigateAction(info, "Change color", null) + result.add(info) + } + } + } + + private class McpColorInfo(private val parent: PsiElement, private val result: McpColorResult) : MergeableLineMarkerInfo( + result.expression, + result.argRange, + ColorIcon(12, result.arg), + Pass.UPDATE_ALL, + Function { result.param.description }, + GutterIconNavigationHandler handler@{ _, _ -> + if (!result.expression.isWritable) { + return@handler + } + + val editor = PsiUtilBase.findEditor(result.expression) ?: return@handler + + val c = ColorChooser.chooseColor(editor.component, "Choose ${result.param.description}", result.arg, result.param.hasAlpha) + if (c != null) { + result.param.setColor(result.withArg(c)) + } + }, + GutterIconRenderer.Alignment.RIGHT + ) { + override fun canMergeWith(info: MergeableLineMarkerInfo<*>) = info is McpColorInfo && info.parent == parent + override fun getCommonIconAlignment(infos: List>) = GutterIconRenderer.Alignment.RIGHT + + override fun getCommonIcon(infos: List>): Icon { + if (infos.size == 2 && infos[0] is McpColorInfo && infos[1] is McpColorInfo) { + return TwoColorsIcon(12, (infos[0] as McpColorInfo).result.arg, (infos[1] as McpColorInfo).result.arg) + } + return AllIcons.Gutter.Colors + } + + override fun getElementPresentation(element: PsiElement): String { + return result.param.description + } + } +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorMethod.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorMethod.kt new file mode 100644 index 000000000..815e1a696 --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorMethod.kt @@ -0,0 +1,334 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.insight.setColor +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.mcp.srg.SrgManager +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.referencedMethod +import com.demonwav.mcdev.util.runWriteAction +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiCall +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiMethod +import java.awt.Color +import java.lang.reflect.Type + +data class McpColorMethod(val member: MemberReference, val srgName: Boolean, val params: List) { + fun match(call: PsiCall): Boolean { + val referenced = call.referencedMethod + return referenced != null && referenced == getMethod(call) + } + + fun extractColors(call: PsiCall): List> { + return params.mapNotNull { it.extractColor(call) } + } + + fun validateCall(call: PsiCall): List> { + if (!match(call)) { + return listOf() + } + return params.flatMap { it.validateCall(call) } + } + + private fun getMethod(context: PsiElement): PsiMethod? { + var reference = member + if (srgName) { + val moduleSrgManager = context.findModule()?.let { MinecraftFacet.getInstance(it, McpModuleType)?.srgManager } + val srgManager = moduleSrgManager ?: SrgManager.findAnyInstance(context.project) + srgManager?.srgMapNow?.mapToMcpMethod(member)?.let { + reference = it + } + } + return reference.resolveMember(context.project) as? PsiMethod + } + + interface Param { + val description: String + val hasAlpha: Boolean + + fun extractColor(call: PsiCall): McpColorResult? + + fun extractColor(result: McpColorResult): Color? + + fun validateCall(call: PsiCall): List> + + fun setColor(context: McpColorResult) + } + + data class SingleIntParam(val position: Int, override val description: String, override val hasAlpha: Boolean) : Param { + override fun extractColor(call: PsiCall): McpColorResult? { + val args = call.argumentList ?: return null + val colorArg = args.expressions.getOrNull(position) as? PsiLiteralExpression ?: return null + val color = extractColor(colorArg) ?: return null + + return McpColorResult(colorArg, this, color) + } + + override fun extractColor(result: McpColorResult): Color? { + return (result.expression as? PsiLiteralExpression)?.let { extractColor(it) } + } + + private fun extractColor(literal: PsiLiteralExpression): Color? { + return Color(literal.value as? Int ?: return null, hasAlpha) + } + + override fun validateCall(call: PsiCall): List> { + val args = call.argumentList ?: return emptyList() + val colorArg = args.expressions.getOrNull(position) as? PsiLiteralExpression ?: return emptyList() + val literal = colorArg.text + + if (!literal.startsWith("0x")) { + return listOf(McpColorResult(colorArg, this, McpColorWarning.NoHex)) + } + + if (hasAlpha && literal.length in 7..8) { + return listOf(McpColorResult(colorArg, this, McpColorWarning.MissingAlpha)) + } + + if (literal.length <= 6) { + return listOf(McpColorResult( + colorArg, + this, + McpColorWarning.MissingComponents( + if (literal.length > 4) listOf("red") else listOf("red", "green") + ) + ) + ) + } + + if (!hasAlpha && literal.length >= 9) { + return listOf(McpColorResult(colorArg, this, McpColorWarning.SuperfluousAlpha)) + } + + return emptyList() + } + + override fun setColor(context: McpColorResult) { + val literal = context.expression as? PsiLiteralExpression ?: return + literal.setColor(context.arg.rgb, hasAlpha) + } + + object Deserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): SingleIntParam { + val obj = json.asJsonObject + return SingleIntParam( + obj["position"]?.asInt ?: 0, + obj["description"]?.asString ?: "Color", + obj["hasAlpha"]?.asBoolean ?: true + ) + } + } + } + + data class FloatVectorParam(val startPosition: Int, override val description: String, override val hasAlpha: Boolean) : Param { + val length = if (hasAlpha) 4 else 3 + val endIndexExclusive = startPosition + length + + override fun extractColor(call: PsiCall): McpColorResult? { + if (validateCall(call).isNotEmpty()) { + return null + } + + val args = call.argumentList ?: return null + val colorArgs = args.expressions.toList().subList(startPosition, endIndexExclusive) + val components = colorArgs.mapNotNull { evaluate(it) } + if (components.size < length) { + return null + } + val r = components[0] + val g = components[1] + val b = components[2] + val a = components.getOrNull(3) ?: 1f + + return McpColorResult(call, this, Color(r, g, b, a), colorArgs[0].textRange.union(colorArgs[length - 1].textRange)) + } + + private fun evaluate(element: PsiElement): Float? { + val facade = JavaPsiFacade.getInstance(element.project) + return facade.constantEvaluationHelper.computeConstantExpression(element) as? Float + } + + override fun extractColor(result: McpColorResult): Color? { + val call = result.expression as? PsiCall ?: return null + return extractColor(call)?.arg + } + + override fun validateCall(call: PsiCall): List> { + val args = call.argumentList ?: return emptyList() + val colorArgs = args.expressions.toList().subList(startPosition, endIndexExclusive) + val components = colorArgs.mapNotNull(::evaluate) + if (components.size < length) { + return emptyList() + } + + val outOfRange = components.withIndex() + .filter { it.value !in 0f..1f } + .map { + McpColorResult( + colorArgs[it.index], + this, + McpColorWarning.ComponentOutOfRange("0.0f", "1.0f") { _ -> + val literal = colorArgs[it.index] + literal.containingFile.runWriteAction { + val node = literal.node + + val literalExpression = JavaPsiFacade.getElementFactory(literal.project) + .createExpressionFromText(it.value.coerceIn(0f, 1f).format(), null) as PsiLiteralExpression + + node.psi.replace(literalExpression) + } + } + ) + }.toList() + + return outOfRange + } + + override fun setColor(context: McpColorResult) { + val call = context.expression as? PsiCall ?: return + val expressions = call.argumentList ?: return + + val color = context.arg + val components = arrayOf(color.red, color.green, color.blue, color.alpha) + + expressions.containingFile.runWriteAction { + val facade = JavaPsiFacade.getElementFactory(expressions.project) + for (i in 0 until length) { + val expression = expressions.expressions[startPosition + i] + val node = expression.node + val value = if (expression is PsiLiteralExpression) (components[i] / 255f).format() else "${components[i]} / 255f" + val newExpression = facade.createExpressionFromText(value, null) + + node.psi.replace(newExpression) + } + } + } + + object Deserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): FloatVectorParam { + val obj = json.asJsonObject + return FloatVectorParam( + obj["startPosition"]?.asInt ?: 0, + obj["description"]?.asString ?: "Color", + obj["hasAlpha"]?.asBoolean ?: true + ) + } + } + } + + data class IntVectorParam(val startIndex: Int, override val description: String, override val hasAlpha: Boolean) : Param { + val length = if (hasAlpha) 4 else 3 + val endIndexExclusive = startIndex + length + + override fun extractColor(call: PsiCall): McpColorResult? { + if (validateCall(call).isNotEmpty()) { + return null + } + + val args = call.argumentList ?: return null + val colorArgs = args.expressions.toList().subList(startIndex, endIndexExclusive) + val components = colorArgs.mapNotNull { evaluate(it) } + if (components.size < length) { + return null + } + val r = components[0] + val g = components[1] + val b = components[2] + val a = components.getOrNull(3) ?: 255 + + return McpColorResult(call, this, Color(r, g, b, a), colorArgs[0].textRange.union(colorArgs[length - 1].textRange)) + } + + private fun evaluate(element: PsiElement): Int? { + val facade = JavaPsiFacade.getInstance(element.project) + return facade.constantEvaluationHelper.computeConstantExpression(element) as? Int + } + + override fun extractColor(result: McpColorResult): Color? { + val call = result.expression as? PsiCall ?: return null + return extractColor(call)?.arg + } + + override fun validateCall(call: PsiCall): List> { + val args = call.argumentList ?: return emptyList() + val colorArgs = args.expressions.toList().subList(startIndex, endIndexExclusive) + val components = colorArgs.mapNotNull(::evaluate) + if (components.size < length) { + return emptyList() + } + + val outOfRange = components.withIndex() + .filter { it.value !in 0..255 } + .map { + McpColorResult( + colorArgs[it.index], + this, + McpColorWarning.ComponentOutOfRange("0", "255") { _ -> + val literal = colorArgs[it.index] + literal.containingFile.runWriteAction { + val node = literal.node + + val literalExpression = JavaPsiFacade.getElementFactory(literal.project) + .createExpressionFromText(it.value.coerceIn(0, 255).toString(), null) as PsiLiteralExpression + + node.psi.replace(literalExpression) + } + } + ) + }.toList() + + return outOfRange + } + + override fun setColor(context: McpColorResult) { + val call = context.expression as? PsiCall ?: return + val expressions = call.argumentList ?: return + + val color = context.arg + val components = arrayOf(color.red, color.green, color.blue, color.alpha) + + expressions.containingFile.runWriteAction { + val facade = JavaPsiFacade.getElementFactory(expressions.project) + for (i in 0 until length) { + val expression = expressions.expressions[startIndex + i] + val node = expression.node + val newExpression = facade.createExpressionFromText(components[i].toString(), null) + + node.psi.replace(newExpression) + } + } + } + + object Deserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): IntVectorParam { + val obj = json.asJsonObject + return IntVectorParam( + obj["startPosition"]?.asInt ?: 0, + obj["description"]?.asString ?: "Color", + obj["hasAlpha"]?.asBoolean ?: true + ) + } + } + } +} + +private fun Float.format(): String { + val number = if (this == 0f || this == 1f) this.toInt().toString() else this.toString() + return "${number}f" +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorMethods.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorMethods.kt new file mode 100644 index 000000000..efa844629 --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorMethods.kt @@ -0,0 +1,117 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod.FloatVectorParam +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod.IntVectorParam +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod.Param +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod.SingleIntParam +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.SemanticVersion +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.psi.PsiElement +import com.intellij.util.io.inputStream +import java.io.InputStream +import java.io.InputStreamReader +import java.lang.reflect.Type +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.streams.toList + +object McpColorMethods { + private val entries by lazy { + val result = load() + result.mapValues { ref -> + result.entries.filter { it.key <= ref.key } + .sortedBy { it.key } + .map { it.value } + .reduce { acc, cur -> + acc.filter { a -> cur.none { b -> a.member == b.member } } + cur + } + } + } + + operator fun get(elem: PsiElement): List { + val module = ModuleUtilCore.findModuleForPsiElement(elem) ?: return emptyList() + val facet = MinecraftFacet.getInstance(module) ?: return emptyList() + return facet.getModuleOfType(McpModuleType)?.colorMethods ?: emptyList() + } + + operator fun get(mcVersion: String): List { + val semVer = SemanticVersion.parse(mcVersion) + return entries.entries.findLast { it.key <= semVer }?.value ?: emptyList() + } + + private fun load(): Map> { + val url = javaClass.getResource("/configs/mcp/colors") + val files = url.toURI().listFiles() + return files + .filter { it.fileName.toString().endsWith(".json") } + .associate { + val version = SemanticVersion.parse(it.fileName.toString().substringBeforeLast('.')) + version to load(it.inputStream()) + } + } + + private fun URI.listFiles(): List { + val parts = this.toString().split("!", limit = 2) + val path = when (parts.size) { + 1 -> Paths.get(this) + else -> { + val env = mutableMapOf() + FileSystems.newFileSystem(URI.create(parts[0]), env).getPath(parts[1]) + } + } + return Files.list(path).toList() + } + + private fun load(stream: InputStream): List { + val content = InputStreamReader(stream) + val gson = GsonBuilder() + .registerTypeAdapter(Param::class.java, McpMethodParamDeserializer) + .registerTypeAdapter(MemberReference::class.java, MemberReferenceDeserializer) + .create() + return gson.fromJson(content, McpColorFile::class.java).entries + } + + class McpColorFile(val entries: List) + + object McpMethodParamDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): Param { + val obj = json.asJsonObject + val discriminator = obj.get("type").asString + return when (discriminator) { + "intvec" -> IntVectorParam.Deserializer.deserialize(json, type, ctx) + "floatvec" -> FloatVectorParam.Deserializer.deserialize(json, type, ctx) + else -> SingleIntParam.Deserializer.deserialize(json, type, ctx) + } + } + } + + object MemberReferenceDeserializer : 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) + } + } +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorResult.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorResult.kt new file mode 100644 index 000000000..0fd50f17a --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorResult.kt @@ -0,0 +1,23 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color + +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement + +data class McpColorResult( + val expression: PsiElement, + val param: McpColorMethod.Param, + val arg: A, + val argRange: TextRange = expression.textRange +) { + fun withArg(arg: A) = McpColorResult(expression, param, arg, argRange) +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorUtil.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorUtil.kt new file mode 100644 index 000000000..c50641a7f --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorUtil.kt @@ -0,0 +1,25 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodCallExpression +import java.awt.Color + +fun PsiElement.findColors(): List> { + if (this !is PsiMethodCallExpression) { + return emptyList() + } + + val method = McpColorMethods[this].find { it.match(this) } ?: return emptyList() + + return method.extractColors(this) +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorWarning.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorWarning.kt new file mode 100644 index 000000000..4e9586db4 --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/McpColorWarning.kt @@ -0,0 +1,24 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color + +sealed class McpColorWarning { + object NoHex : McpColorWarning() + + data class MissingComponents(val components: List) : McpColorWarning() + + object MissingAlpha : McpColorWarning() + + object SuperfluousAlpha : McpColorWarning() + + data class ComponentOutOfRange(val min: String, val max: String, val clamp: (McpColorResult) -> Unit) : McpColorWarning() +} + diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorComponentOutOfRangeInspection.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorComponentOutOfRangeInspection.kt new file mode 100644 index 000000000..67d804fdf --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorComponentOutOfRangeInspection.kt @@ -0,0 +1,67 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color.inspections + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod +import com.demonwav.mcdev.platform.mcp.color.McpColorMethods +import com.demonwav.mcdev.platform.mcp.color.McpColorResult +import com.demonwav.mcdev.platform.mcp.color.McpColorWarning +import com.demonwav.mcdev.platform.mcp.color.findColors +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiCallExpression +import com.siyeh.ig.BaseInspection +import com.siyeh.ig.BaseInspectionVisitor +import com.siyeh.ig.InspectionGadgetsFix +import org.jetbrains.annotations.Nls + +class ColorComponentOutOfRangeInspection : BaseInspection() { + @Nls + override fun getDisplayName(): String { + return "MCP Color component out of range" + } + + override fun buildErrorString(vararg infos: Any): String { + return "Color component is out of [${infos[1]},${infos[2]}] range, this can lead to unexpected behavior." + } + + override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { + val result = infos[0] as? McpColorResult ?: return null + val clamp = infos[3] as? (McpColorResult) -> Unit ?: return null + return object : InspectionGadgetsFix() { + override fun doFix(project: Project, descriptor: ProblemDescriptor) { + clamp(result) + } + + @Nls + override fun getName() = "Clamp value to range" + + @Nls + override fun getFamilyName() = "MCP Colors" + } + } + + override fun buildVisitor(): BaseInspectionVisitor { + return object : BaseInspectionVisitor() { + override fun visitCallExpression(call: PsiCallExpression) { + val results = McpColorMethods[call].flatMap { it.validateCall(call) } + for (result in results) { + if (result.arg is McpColorWarning.ComponentOutOfRange) { + registerError(result.expression, result, result.arg.min, result.arg.max, result.arg.clamp) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorMissingAlphaInspection.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorMissingAlphaInspection.kt new file mode 100644 index 000000000..14c3cf166 --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorMissingAlphaInspection.kt @@ -0,0 +1,65 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color.inspections + +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod +import com.demonwav.mcdev.platform.mcp.color.McpColorMethods +import com.demonwav.mcdev.platform.mcp.color.McpColorResult +import com.demonwav.mcdev.platform.mcp.color.McpColorWarning +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiCallExpression +import com.siyeh.ig.BaseInspection +import com.siyeh.ig.BaseInspectionVisitor +import com.siyeh.ig.InspectionGadgetsFix +import org.jetbrains.annotations.Nls +import java.awt.Color + +class ColorMissingAlphaInspection : BaseInspection() { + @Nls + override fun getDisplayName(): String { + return "MCP Color missing alpha component" + } + + override fun buildErrorString(vararg infos: Any): String { + return "This method expects its color argument to have an alpha component. " + + "Without an explicit alpha value, the color will be considered fully transparent." + } + + override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { + val result = infos[0] as? McpColorResult ?: return null + return object : InspectionGadgetsFix() { + override fun doFix(project: Project, descriptor: ProblemDescriptor) { + val color = result.param.extractColor(result) ?: return + result.param.setColor(result.withArg(Color(0xFF000000.toInt() or color.rgb, true))) + } + + @Nls + override fun getName() = "Add fully opaque alpha component" + + @Nls + override fun getFamilyName() = "MCP Colors" + } + } + + override fun buildVisitor(): BaseInspectionVisitor { + return object : BaseInspectionVisitor() { + override fun visitCallExpression(call: PsiCallExpression) { + val results = McpColorMethods[call].flatMap { it.validateCall(call) } + for (result in results) { + if (result.arg == McpColorWarning.MissingAlpha) { + registerError(result.expression, result) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorSuperfluousAlphaInspection.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorSuperfluousAlphaInspection.kt new file mode 100644 index 000000000..b4d19c728 --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/ColorSuperfluousAlphaInspection.kt @@ -0,0 +1,64 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color.inspections + +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod +import com.demonwav.mcdev.platform.mcp.color.McpColorMethods +import com.demonwav.mcdev.platform.mcp.color.McpColorResult +import com.demonwav.mcdev.platform.mcp.color.McpColorWarning +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiCallExpression +import com.siyeh.ig.BaseInspection +import com.siyeh.ig.BaseInspectionVisitor +import com.siyeh.ig.InspectionGadgetsFix +import org.jetbrains.annotations.Nls +import java.awt.Color + +class ColorSuperfluousAlphaInspection : BaseInspection() { + @Nls + override fun getDisplayName(): String { + return "MCP Color superfluous alpha component" + } + + override fun buildErrorString(vararg infos: Any): String { + return "This method does not expect an alpha component" + } + + override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { + val result = infos[0] as? McpColorResult ?: return null + return object : InspectionGadgetsFix() { + override fun doFix(project: Project, descriptor: ProblemDescriptor) { + val color = result.param.extractColor(result) ?: return + result.param.setColor(result.withArg(Color(0xFFFFFF and color.rgb, false))) + } + + @Nls + override fun getName() = "Remove alpha component" + + @Nls + override fun getFamilyName() = "MCP Colors" + } + } + + override fun buildVisitor(): BaseInspectionVisitor { + return object : BaseInspectionVisitor() { + override fun visitCallExpression(call: PsiCallExpression) { + val results = McpColorMethods[call].flatMap { it.validateCall(call) } + for (result in results) { + if (result.arg == McpColorWarning.SuperfluousAlpha) { + registerError(result.expression, result) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/MissingColorComponentInspection.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/MissingColorComponentInspection.kt new file mode 100644 index 000000000..3651bb25e --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/MissingColorComponentInspection.kt @@ -0,0 +1,65 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color.inspections + +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod +import com.demonwav.mcdev.platform.mcp.color.McpColorMethods +import com.demonwav.mcdev.platform.mcp.color.McpColorResult +import com.demonwav.mcdev.platform.mcp.color.McpColorWarning +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiCallExpression +import com.siyeh.ig.BaseInspection +import com.siyeh.ig.BaseInspectionVisitor +import com.siyeh.ig.InspectionGadgetsFix +import org.jetbrains.annotations.Nls +import java.awt.Color + +class MissingColorComponentInspection : BaseInspection() { + @Nls + override fun getDisplayName(): String { + return "MCP Color missing one or more component" + } + + override fun buildErrorString(vararg infos: Any): String { + return "Color missing ${(infos[1] as List).joinToString(" and ")} component (implied as zero)" + } + + override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { + val result = infos[0] as? McpColorResult ?: return null + return object : InspectionGadgetsFix() { + override fun doFix(project: Project, descriptor: ProblemDescriptor) { + val color = result.param.extractColor(result) ?: return + val newColor = if (!result.param.hasAlpha) 0xFFFFFF and color.rgb else color.rgb + result.param.setColor(result.withArg(Color(newColor, result.param.hasAlpha))) + } + + @Nls + override fun getName() = "Pad color with zero components" + + @Nls + override fun getFamilyName() = "MCP Colors" + } + } + + override fun buildVisitor(): BaseInspectionVisitor { + return object : BaseInspectionVisitor() { + override fun visitCallExpression(call: PsiCallExpression) { + val results = McpColorMethods[call].flatMap { it.validateCall(call) } + for (result in results) { + if (result.arg is McpColorWarning.MissingComponents) { + registerError(result.expression, result, result.arg.components) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/NonHexColorInspection.kt b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/NonHexColorInspection.kt new file mode 100644 index 000000000..1c1c2f46a --- /dev/null +++ b/src/main/kotlin/com/demonwav/mcdev/platform/mcp/color/inspections/NonHexColorInspection.kt @@ -0,0 +1,65 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2018 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.mcp.color.inspections + +import com.demonwav.mcdev.platform.mcp.color.McpColorMethod +import com.demonwav.mcdev.platform.mcp.color.McpColorMethods +import com.demonwav.mcdev.platform.mcp.color.McpColorResult +import com.demonwav.mcdev.platform.mcp.color.McpColorWarning +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiCallExpression +import com.siyeh.ig.BaseInspection +import com.siyeh.ig.BaseInspectionVisitor +import com.siyeh.ig.InspectionGadgetsFix +import org.jetbrains.annotations.Nls +import java.awt.Color + +class NonHexColorInspection : BaseInspection() { + @Nls + override fun getDisplayName(): String { + return "MCP Color using non-hex literal" + } + + override fun buildErrorString(vararg infos: Any): String { + return "Color arguments should use hex literal for easily identifying color components." + } + + override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { + val result = infos[0] as? McpColorResult ?: return null + return object : InspectionGadgetsFix() { + override fun doFix(project: Project, descriptor: ProblemDescriptor) { + val color = result.param.extractColor(result) ?: return + val newColor = if (!result.param.hasAlpha) 0xFFFFFF and color.rgb else color.rgb + result.param.setColor(result.withArg(Color(newColor, result.param.hasAlpha))) + } + + @Nls + override fun getName() = "Convert to hex literal" + + @Nls + override fun getFamilyName() = "MCP Colors" + } + } + + override fun buildVisitor(): BaseInspectionVisitor { + return object : BaseInspectionVisitor() { + override fun visitCallExpression(call: PsiCallExpression) { + val results = McpColorMethods[call].flatMap { it.validateCall(call) } + for (result in results) { + if (result.arg == McpColorWarning.NoHex) { + registerError(result.expression, result) + } + } + } + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 0bcceb2b0..4ddd55ae9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -203,6 +203,12 @@ + + + + + + @@ -378,6 +384,42 @@ + + + + + + + description="Copy the reference to the element for call in an injector">