Skip to content

[WIP] Add specialized MCP color support #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/main/kotlin/com/demonwav/mcdev/insight/ColorUtil.kt
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions src/main/kotlin/com/demonwav/mcdev/platform/mcp/McpModule.kt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PsiElement>, result: MutableCollection<LineMarkerInfo<PsiElement>>) {
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<Color>) : MergeableLineMarkerInfo<PsiElement>(
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<MergeableLineMarkerInfo<*>>) = GutterIconRenderer.Alignment.RIGHT

override fun getCommonIcon(infos: List<MergeableLineMarkerInfo<*>>): 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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Param>) {
fun match(call: PsiCall): Boolean {
val referenced = call.referencedMethod
return referenced != null && referenced == getMethod(call)
}

fun extractColors(call: PsiCall): List<McpColorResult<Color>> {
return params.mapNotNull { it.extractColor(call) }
}

fun validateCall(call: PsiCall): List<McpColorResult<McpColorWarning>> {
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<Color>?

fun extractColor(result: McpColorResult<Any>): Color?

fun validateCall(call: PsiCall): List<McpColorResult<McpColorWarning>>

fun setColor(context: McpColorResult<Color>)
}

data class SingleIntParam(val position: Int, override val description: String, override val hasAlpha: Boolean) : Param {
override fun extractColor(call: PsiCall): McpColorResult<Color>? {
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<Any>): 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<McpColorResult<McpColorWarning>> {
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<Color>) {
val literal = context.expression as? PsiLiteralExpression ?: return
literal.setColor(context.arg.rgb, hasAlpha)
}

object Deserializer : JsonDeserializer<SingleIntParam> {
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<Color>? {
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<Any>): Color? {
val call = result.expression as? PsiCall ?: return null
return extractColor(call)?.arg
}

override fun validateCall(call: PsiCall): List<McpColorResult<McpColorWarning>> {
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<Color>) {
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<FloatVectorParam> {
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<Color>? {
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<Any>): Color? {
val call = result.expression as? PsiCall ?: return null
return extractColor(call)?.arg
}

override fun validateCall(call: PsiCall): List<McpColorResult<McpColorWarning>> {
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<Color>) {
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<IntVectorParam> {
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"
}
Original file line number Diff line number Diff line change
@@ -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<McpColorMethod> {
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<McpColorMethod> {
val semVer = SemanticVersion.parse(mcVersion)
return entries.entries.findLast { it.key <= semVer }?.value ?: emptyList()
}

private fun load(): Map<SemanticVersion, List<McpColorMethod>> {
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<Path> {
val parts = this.toString().split("!", limit = 2)
val path = when (parts.size) {
1 -> Paths.get(this)
else -> {
val env = mutableMapOf<String, String>()
FileSystems.newFileSystem(URI.create(parts[0]), env).getPath(parts[1])
}
}
return Files.list(path).toList()
}

private fun load(stream: InputStream): List<McpColorMethod> {
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<McpColorMethod>)

object McpMethodParamDeserializer : JsonDeserializer<Param> {
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<MemberReference> {
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<out A>(
val expression: PsiElement,
val param: McpColorMethod.Param,
val arg: A,
val argRange: TextRange = expression.textRange
) {
fun <A> withArg(arg: A) = McpColorResult(expression, param, arg, argRange)
}
Original file line number Diff line number Diff line change
@@ -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<McpColorResult<Color>> {
if (this !is PsiMethodCallExpression) {
return emptyList()
}

val method = McpColorMethods[this].find { it.match(this) } ?: return emptyList()

return method.extractColors(this)
}
Original file line number Diff line number Diff line change
@@ -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<String>) : McpColorWarning()

object MissingAlpha : McpColorWarning()

object SuperfluousAlpha : McpColorWarning()

data class ComponentOutOfRange(val min: String, val max: String, val clamp: (McpColorResult<Any>) -> Unit) : McpColorWarning()
}

Original file line number Diff line number Diff line change
@@ -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() {
Copy link
Contributor

@stephan-gh stephan-gh Oct 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid introducing new inspections built on BaseInspection? In my opinion that class is pretty much obsolete and most things can be implemented with one of IntelliJ's base classes (here: AbstractBaseJavaLocalInspectionTool) in a nicer way.

@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<McpColorWarning> ?: return null
val clamp = infos[3] as? (McpColorResult<Any>) -> 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)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<McpColorWarning> ?: 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)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<McpColorWarning> ?: 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)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String>).joinToString(" and ")} component (implied as zero)"
}

override fun buildFix(vararg infos: Any): InspectionGadgetsFix? {
val result = infos[0] as? McpColorResult<McpColorWarning> ?: 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)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<McpColorWarning> ?: 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)
}
}
}
}
}
}
44 changes: 43 additions & 1 deletion src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
@@ -203,6 +203,12 @@

<runConfigurationExtension implementation="com.demonwav.mcdev.platform.mcp.debug.McpRunConfigurationExtension"/>

<!-- MCP Line Marker Provider -->
<codeInsight.lineMarkerProvider language="" implementationClass="com.demonwav.mcdev.platform.mcp.color.McpColorLineMarkerProvider"/>

<!-- MCP Annotator -->
<annotator language="JAVA" implementationClass="com.demonwav.mcdev.platform.mcp.color.McpColorAnnotator"/>

<!-- access transformer file type -->
<fileTypeFactory implementation="com.demonwav.mcdev.platform.mcp.at.AtFileTypeFactory"/>
<lang.parserDefinition language="Access Transformers" implementationClass="com.demonwav.mcdev.platform.mcp.at.AtParserDefinition"/>
@@ -378,6 +384,42 @@
<!--endregion-->

<!--region MCP INSPECTIONS-->
<localInspection displayName="MCP Color using non-hex literal"
groupName="MCP"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mcp.color.inspections.NonHexColorInspection"/>
<localInspection displayName="MCP Color missing one or more component"
groupName="MCP"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mcp.color.inspections.MissingColorComponentInspection"/>
<localInspection displayName="MCP Color missing alpha component"
groupName="MCP"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mcp.color.inspections.ColorMissingAlphaInspection"/>
<localInspection displayName="MCP Color superfluous alpha component"
groupName="MCP"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mcp.color.inspections.ColorSuperfluousAlphaInspection"/>
<localInspection displayName="MCP Color component out of range"
groupName="MCP"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mcp.color.inspections.ColorComponentOutOfRangeInspection"/>

<localInspection displayName="MCP Entity class missing World constructor"
groupName="MCP"
language="JAVA"
@@ -763,7 +805,7 @@
</action>
<action class="com.demonwav.mcdev.platform.mixin.action.CopyMixinTargetReferenceAction" id="CopyMixinTargetReferenceAction"
text="Copy Mixin target reference"
description="Copy the reference to the element for use in an injector">
description="Copy the reference to the element for call in an injector">
<add-to-group relative-to-action="EditorPopupMenu2" anchor="after" group-id="EditorPopupMenu"/>
</action>
<action class="com.demonwav.mcdev.platform.mcp.actions.FindSrgMappingAction" id="FindSrgMappingAction"
127 changes: 127 additions & 0 deletions src/main/resources/configs/mcp/colors/1.12.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{
"entries": [
{
"__comment": "drawRect",
"member": "net.minecraft.client.gui.Gui#func_73734_a(IIIII)V",
"srgName": true,
"params": [
{
"type": "int",
"description": "Rectangle Color",
"position": 4,
"hasAlpha": true
}
]
},
{
"__comment": "drawHorizontalLine",
"member": "net.minecraft.client.gui.Gui#func_73730_a(IIII)V",
"srgName": true,
"params": [
{
"type": "int",
"description": "Line Color",
"position": 3,
"hasAlpha": true
}
]
},
{
"__comment": "drawVerticalLine",
"member": "net.minecraft.client.gui.Gui#func_73728_b(IIII)V",
"srgName": true,
"params": [
{
"type": "int",
"description": "Line Color",
"position": 3,
"hasAlpha": true
}
]
},
{
"__comment": "drawGradientRect",
"member": "net.minecraft.client.gui.Gui#func_73733_a(IIIIII)V",
"srgName": true,
"params": [
{
"type": "int",
"description": "Start Color",
"position": 4,
"hasAlpha": true
},
{
"type": "int",
"description": "End Color",
"position": 5,
"hasAlpha": true
}
]
},
{
"__comment": "clearColor",
"member": "net.minecraft.client.renderer.GlStateManager#func_179082_a(FFFF)V",
"srgName": true,
"params": [
{
"type": "floatvec",
"description": "Clear Color",
"startPosition": 0,
"hasAlpha": true
}
]
},
{
"__comment": "color4f",
"member": "net.minecraft.client.renderer.GlStateManager#func_179131_c(FFFF)V",
"srgName": true,
"params": [
{
"type": "floatvec",
"description": "Color Multiplier",
"startPosition": 0,
"hasAlpha": true
}
]
},
{
"__comment": "color3f",
"member": "net.minecraft.client.renderer.GlStateManager#func_179124_c(FFF)V",
"srgName": true,
"params": [
{
"type": "floatvec",
"description": "Color Multiplier",
"startPosition": 0,
"hasAlpha": false
}
]
},
{
"__comment": "color4f",
"member": "net.minecraft.client.renderer.BufferBuilder#func_181666_a(FFFF)Lnet/minecraft/client/renderer/BufferBuilder;",
"srgName": true,
"params": [
{
"type": "floatvec",
"description": "Color Multiplier",
"startPosition": 0,
"hasAlpha": true
}
]
},
{
"__comment": "color4i",
"member": "net.minecraft.client.renderer.BufferBuilder#func_181669_b(IIII)Lnet/minecraft/client/renderer/BufferBuilder;",
"srgName": true,
"params": [
{
"type": "intvec",
"description": "Color Multiplier",
"startPosition": 0,
"hasAlpha": true
}
]
}
]
}