Skip to content

Commit a84161e

Browse files
committed
Make color parsing logic more robust
So we can now handle signatures like (Int, Int, Int, Float)
1 parent 7f4bdac commit a84161e

File tree

1 file changed

+135
-90
lines changed

1 file changed

+135
-90
lines changed

plugin/src/main/kotlin/com/varabyte/kobweb/intellij/colors/KobwebColorProvider.kt

+135-90
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.jetbrains.kotlin.resolve.BindingContext
1818
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
1919
import java.awt.Color
2020
import kotlin.math.abs
21+
import kotlin.math.roundToInt
2122

2223
/**
2324
* This constant prevents the color tracer from following references ridiculously deep into the codebase.
@@ -57,6 +58,10 @@ class KobwebColorProvider : ElementColorProvider {
5758
override fun setColorTo(element: PsiElement, color: Color) = Unit
5859
}
5960

61+
// navigationElement returns the element where a feature like "Go to declaration" would point:
62+
// The source declaration, if found, and not a compiled one, which would make further analyzing impossible.
63+
private fun KtSimpleNameExpression.findDeclaration(): PsiElement? = this.mainReference.resolve()?.navigationElement
64+
6065
/**
6166
* Tries resolving references as deep as possible and checks if a Kobweb color is being referred to.
6267
*
@@ -82,13 +87,10 @@ private fun traceColor(element: PsiElement, currentDepth: Int = 0): Color? {
8287

8388
is KtPropertyAccessor -> element.bodyExpression
8489

85-
is KtCallExpression -> null.also {
86-
val calleeExpression = element.calleeExpression as? KtNameReferenceExpression ?: return@also
87-
val callee = calleeExpression.findDeclaration() as? KtNamedFunction ?: return@also
88-
89-
tryParseKobwebColorFunctionCall(callee, element.valueArguments)?.let { parsedColor ->
90-
return parsedColor
91-
}
90+
is KtCallExpression -> {
91+
val color = element.tryParseKobwebColorFunctionColor()
92+
if (color != null) return color
93+
null
9294
}
9395

9496
else -> null
@@ -99,105 +101,144 @@ private fun traceColor(element: PsiElement, currentDepth: Int = 0): Color? {
99101
} else null
100102
}
101103

104+
private fun Float.toColorInt(): Int {
105+
return (this * 255f).roundToInt().coerceIn(0, 255)
106+
}
107+
102108
/**
103-
* Checks if a called function is a Kobweb color function and if it is, tries extracting the color from the call.
104-
*
105-
* @param callee The function being called, that might be a Kobweb color function
106-
* @param valueArguments The arguments the [callee] is called with
109+
* Checks if a call expression represents a Kobweb color function call and if so, try extracting the color from it.
107110
*
108111
* @return The specified color, if it could be parsed and the callee is a Kobweb color function, otherwise null
109112
*/
110-
private fun tryParseKobwebColorFunctionCall(
111-
callee: KtNamedFunction,
112-
valueArguments: Collection<KtValueArgument>
113-
): Color? = with(valueArguments) {
114-
when {
115-
callee.isKobwebColorFunction("rgb(r: Int, g: Int, b: Int)") ->
116-
evaluateArguments<Int>(3)?.let { args ->
117-
tryCreateRgbColor(args[0], args[1], args[2])
118-
}
119-
120-
callee.isKobwebColorFunction("rgb(value: Int)") ->
121-
evaluateArguments<Int>(1)?.let { args ->
122-
tryCreateRgbColor(args[0])
123-
}
124-
125-
callee.isKobwebColorFunction("rgba(value: Int)", "rgba(value: Long)") ->
126-
(evaluateArguments<Int>(1) ?: evaluateArguments<Long, Int>(1) { it.toInt() })?.let { args ->
127-
tryCreateRgbColor(args[0] shr Byte.SIZE_BITS)
128-
}
129-
130-
callee.isKobwebColorFunction("argb(value: Int)", "argb(value: Long)") ->
131-
(evaluateArguments<Int>(1) ?: evaluateArguments<Long, Int>(1) { it.toInt() })?.let { args ->
132-
tryCreateRgbColor(args[0] and 0x00_FF_FF_FF)
133-
}
134-
135-
callee.isKobwebColorFunction("hsl(h: Float, s: Float, l: Float)") ->
136-
evaluateArguments<Float>(3)?.let { args ->
137-
tryCreateHslColor(args[0], args[1], args[2])
138-
}
139-
140-
callee.isKobwebColorFunction("hsla(h: Float, s: Float, l: Float, a: Float)") ->
141-
evaluateArguments<Float>(4)?.let { args ->
142-
tryCreateHslColor(args[0], args[1], args[2])
143-
}
113+
private fun KtCallExpression.tryParseKobwebColorFunctionColor(): Color? {
114+
"$KOBWEB_COLOR_COMPANION_FQ_NAME.rgb".let { rgbFqn ->
115+
this.extractConstantArguments1<Int>(rgbFqn)?.let { (rgb) ->
116+
return tryCreateRgbColor(rgb)
117+
}
144118

145-
else -> null
119+
this.extractConstantArguments1<Long>(rgbFqn)?.let { (rgb) ->
120+
return tryCreateRgbColor(rgb.toInt())
121+
}
122+
123+
this.extractConstantArguments3<Int, Int, Int>(rgbFqn)?.let { (r, g, b) ->
124+
return tryCreateRgbColor(r, g, b)
125+
}
126+
127+
this.extractConstantArguments3<Float, Float, Float>(rgbFqn)?.let { (r, g, b) ->
128+
return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
129+
}
146130
}
147-
}
148131

132+
"$KOBWEB_COLOR_COMPANION_FQ_NAME.rgba".let { rgbaFqn ->
133+
this.extractConstantArguments1<Int>(rgbaFqn)?.let { (rgb) ->
134+
return tryCreateRgbColor(rgb shr 8)
135+
}
149136

150-
// navigationElement returns the element where a feature like "Go to declaration" would point:
151-
// The source declaration, if found, and not a compiled one, which would make further analyzing impossible.
152-
private fun KtSimpleNameExpression.findDeclaration(): PsiElement? = this.mainReference.resolve()?.navigationElement
137+
this.extractConstantArguments1<Long>(rgbaFqn)?.let { (rgb) ->
138+
return tryCreateRgbColor(rgb.toInt() shr 8)
139+
}
153140

154-
/**
155-
* Evaluates a collection of value arguments to the specified type.
156-
*
157-
* For example, if we have a collection of decimal, hex, and binary arguments,
158-
* this method can parse them into regular integer values, so 123, 0x7B and 0b0111_1011
159-
* would all evaluate to 123.
160-
*
161-
* @param argCount The size the original and evaluated collections must have. If this value disagrees with the size of
162-
* the passed in collection, it will throw an exception; it's essentially treated like an assertion at that point.
163-
* Otherwise, it's used to avoid returning a final, evaluated array of unexpected size.
164-
* @param evaluatedValueMapper Convenience parameter to avoid having to type `.map { ... }.toTypedArray()`
165-
*
166-
* @return the evaluated arguments of length [argCount] if evaluation of **all** arguments succeeded,
167-
* and [argCount] elements were passed for evaluation, otherwise null
168-
*/
169-
private inline fun <reified Evaluated, reified Mapped> Collection<KtValueArgument>.evaluateArguments(
170-
argCount: Int,
171-
evaluatedValueMapper: (Evaluated) -> Mapped
172-
): Array<Mapped>? {
141+
this.extractConstantArguments4<Int, Int, Int, Int>(rgbaFqn)?.let { (r, g, b) ->
142+
return tryCreateRgbColor(r, g, b)
143+
}
144+
145+
this.extractConstantArguments4<Int, Int, Int, Float>(rgbaFqn)?.let { (r, g, b) ->
146+
return tryCreateRgbColor(r, g, b)
147+
}
148+
149+
this.extractConstantArguments4<Float, Float, Float, Float>(rgbaFqn)?.let { (r, g, b) ->
150+
return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
151+
}
152+
}
153+
154+
"$KOBWEB_COLOR_COMPANION_FQ_NAME.argb".let { argbFqn ->
155+
this.extractConstantArguments1<Int>(argbFqn)?.let { (rgb) ->
156+
return tryCreateRgbColor(rgb and 0x00_FF_FF_FF)
157+
}
158+
159+
this.extractConstantArguments1<Long>(argbFqn)?.let { (rgb) ->
160+
return tryCreateRgbColor(rgb.toInt() and 0x00_FF_FF_FF)
161+
}
173162

174-
check(this.size == argCount) { "evaluateArguments called on a collection expecting $argCount arguments, but it only had ${this.size}"}
163+
this.extractConstantArguments4<Int, Int, Int, Int>(argbFqn)?.let { (_, r, g, b) ->
164+
return tryCreateRgbColor(r, g, b)
165+
}
175166

176-
val constantExpressions = this.mapNotNull { it.getArgumentExpression() as? KtConstantExpression }
167+
this.extractConstantArguments4<Float, Int, Int, Int>(argbFqn)?.let { (_, r, g, b) ->
168+
return tryCreateRgbColor(r, g, b)
169+
}
177170

178-
val evaluatedArguments = constantExpressions.mapNotNull { expr ->
179-
val bindingContext = expr.analyze(BodyResolveMode.PARTIAL)
180-
val constant = bindingContext.get(BindingContext.COMPILE_TIME_VALUE, expr) ?: return@mapNotNull null
181-
val type = bindingContext.getType(expr) ?: return@mapNotNull null
182-
constant.getValue(type) as? Evaluated
171+
this.extractConstantArguments4<Float, Float, Float, Float>(argbFqn)?.let { (_, r, g, b) ->
172+
return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
173+
}
183174
}
184175

185-
return if (evaluatedArguments.size != argCount) null
186-
else evaluatedArguments.map(evaluatedValueMapper).toTypedArray()
187-
}
176+
"$KOBWEB_COLOR_COMPANION_FQ_NAME.hsl".let { hslFqn ->
177+
this.extractConstantArguments3<Int, Float, Float>(hslFqn)?.let { (h, s, l) ->
178+
return tryCreateHslColor(h, s, l)
179+
}
188180

189-
private inline fun <reified Evaluated> Collection<KtValueArgument>.evaluateArguments(argCount: Int) =
190-
evaluateArguments<Evaluated, Evaluated>(argCount) { it }
181+
this.extractConstantArguments3<Float, Float, Float>(hslFqn)?.let { (h, s, l) ->
182+
return tryCreateHslColor(h.roundToInt(), s, l)
183+
}
184+
}
191185

192-
private fun KtNamedFunction.isKobwebColorFunction(vararg functionSignatures: String): Boolean {
193-
val actualFqName = this.kotlinFqName?.asString() ?: return false
194-
val actualParameters = this.valueParameterList?.text ?: return false
195-
val actual = actualFqName + actualParameters
186+
"$KOBWEB_COLOR_COMPANION_FQ_NAME.hsla".let { hslaFqn ->
187+
this.extractConstantArguments4<Int, Float, Float, Float>(hslaFqn)?.let { (h, s, l) ->
188+
return tryCreateHslColor(h, s, l)
189+
}
196190

197-
return functionSignatures.any { functionSignature ->
198-
val expected = "$KOBWEB_COLOR_COMPANION_FQ_NAME.$functionSignature"
199-
expected == actual
191+
this.extractConstantArguments4<Float, Float, Float, Float>(hslaFqn)?.let { (h, s, l) ->
192+
return tryCreateHslColor(h.roundToInt(), s, l)
193+
}
200194
}
195+
196+
return null
197+
}
198+
199+
private data class Values<T1, T2, T3, T4>(
200+
val v1: T1,
201+
val v2: T2,
202+
val v3: T3,
203+
val v4: T4
204+
)
205+
206+
private inline fun <reified T> KtValueArgument.extractConstantValue(): T? {
207+
val constantExpression = getArgumentExpression() as? KtConstantExpression ?: return null
208+
val bindingContext = constantExpression.analyze(BodyResolveMode.PARTIAL)
209+
val constant = bindingContext.get(BindingContext.COMPILE_TIME_VALUE, constantExpression) ?: return null
210+
val type = bindingContext.getType(constantExpression) ?: return null
211+
return constant.getValue(type) as? T
212+
}
213+
214+
private fun KtCallExpression.valueArgumentsIf(fqn: String, requiredSize: Int): List<KtValueArgument>? {
215+
val calleeExpression = calleeExpression as? KtNameReferenceExpression ?: return null
216+
val callee = calleeExpression.findDeclaration() as? KtNamedFunction ?: return null
217+
if (callee.kotlinFqName?.asString() != fqn) return null
218+
return valueArguments.takeIf { it.size == requiredSize }
219+
}
220+
221+
private inline fun <reified I> KtCallExpression.extractConstantArguments1(fqn: String): Values<I, Unit, Unit, Unit>? {
222+
val valueArguments = valueArgumentsIf(fqn, 1) ?: return null
223+
val v1: I? = valueArguments[0].extractConstantValue()
224+
return if (v1 != null) Values(v1, Unit, Unit, Unit) else null
225+
}
226+
227+
private inline fun <reified I1, reified I2, reified I3> KtCallExpression.extractConstantArguments3(fqn: String): Values<I1, I2, I3, Unit>? {
228+
val valueArguments = valueArgumentsIf(fqn, 3) ?: return null
229+
val v1: I1? = valueArguments[0].extractConstantValue()
230+
val v2: I2? = valueArguments[1].extractConstantValue()
231+
val v3: I3? = valueArguments[2].extractConstantValue()
232+
return if (v1 != null && v2 != null && v3 != null) Values(v1, v2, v3, Unit) else null
233+
}
234+
235+
private inline fun <reified I1, reified I2, reified I3, reified I4> KtCallExpression.extractConstantArguments4(fqn: String): Values<I1, I2, I3, I4>? {
236+
val valueArguments = valueArgumentsIf(fqn, 4) ?: return null
237+
val v1: I1? = valueArguments[0].extractConstantValue()
238+
val v2: I2? = valueArguments[1].extractConstantValue()
239+
val v3: I3? = valueArguments[2].extractConstantValue()
240+
val v4: I4? = valueArguments[3].extractConstantValue()
241+
return if (v1 != null && v2 != null && v3 != null && v4 != null) Values(v1, v2, v3, v4) else null
201242
}
202243

203244
private fun tryCreateRgbColor(r: Int, g: Int, b: Int) =
@@ -206,11 +247,15 @@ private fun tryCreateRgbColor(r: Int, g: Int, b: Int) =
206247
private fun tryCreateRgbColor(rgb: Int) =
207248
runCatching { Color(rgb) }.getOrNull()
208249

209-
private fun tryCreateHslColor(hue: Float, saturation: Float, lightness: Float): Color? {
250+
// Expected values:
251+
// hue: 0-360
252+
// saturation: 0-1
253+
// lightness: 0-1
254+
private fun tryCreateHslColor(hue: Int, saturation: Float, lightness: Float): Color? {
210255
// https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
211256
val chroma = (1 - abs(2 * lightness - 1)) * saturation
212257
val intermediateValue = chroma * (1 - abs(((hue / 60) % 2) - 1))
213-
val hueSection = (hue.toInt() % 360) / 60
258+
val hueSection = (hue % 360) / 60
214259
val r: Float
215260
val g: Float
216261
val b: Float

0 commit comments

Comments
 (0)