Skip to content

Commit a61eb75

Browse files
committed
feat: hint resources ref'd by matchLabels/-Expressions and nav editor to them (#642)
1 parent 899bcc2 commit a61eb75

File tree

5 files changed

+263
-7
lines changed

5 files changed

+263
-7
lines changed

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import com.intellij.psi.PsiFile
2929
import com.intellij.ui.dsl.builder.panel
3030
import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo
3131
import com.redhat.devtools.intellij.kubernetes.editor.inlay.base64.Base64Presentations
32+
import com.redhat.devtools.intellij.kubernetes.editor.inlay.selector.SelectorPresentations
3233
import org.jetbrains.yaml.psi.YAMLFile
3334
import javax.swing.JComponent
3435

@@ -81,7 +82,8 @@ internal class ResourceEditorInlayHintsProvider : InlayHintsProvider<NoSettings>
8182
file.documents.forEach { document ->
8283
val info = KubernetesTypeInfo.create(document) ?: return@forEach
8384
val element = document.topLevelValue ?: return@forEach
84-
Base64Presentations.create(element, info, sink, editor)?.create()
85+
Base64Presentations.create(element, info, sink, editor)
86+
SelectorPresentations.create(element, file, info, sink, editor)
8587
}
8688
}
8789
}
@@ -90,7 +92,7 @@ internal class ResourceEditorInlayHintsProvider : InlayHintsProvider<NoSettings>
9092
return ReadAction.run<Exception> {
9193
val info = KubernetesTypeInfo.create(file) ?: return@run
9294
val element = file.topLevelValue ?: return@run
93-
Base64Presentations.create(element, info, sink, editor)?.create()
95+
Base64Presentations.create(element, info, sink, editor)
9496
}
9597
}
9698

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64Presentations.kt

+4-5
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ object Base64Presentations {
4040
private const val SECRET_RESOURCE_KIND = "Secret"
4141
private const val CONFIGMAP_RESOURCE_KIND = "ConfigMap"
4242

43-
fun create(element: PsiElement, info: KubernetesTypeInfo, sink: InlayHintsSink, editor: Editor): InlayPresentationsFactory? {
43+
fun create(element: PsiElement, info: KubernetesTypeInfo, sink: InlayHintsSink, editor: Editor): Collection<InlayPresentation>? {
4444
return when {
4545
isKubernetesResource(SECRET_RESOURCE_KIND, info) -> {
4646
val data = getDataValue(element) ?: return null
47-
StringPresentationsFactory(data, sink, editor)
47+
StringPresentationsFactory(data, sink, editor).create()
4848
}
4949

5050
isKubernetesResource(CONFIGMAP_RESOURCE_KIND, info) -> {
5151
val binaryData = getBinaryData(element) ?: return null
52-
BinaryPresentationsFactory(binaryData, sink, editor)
52+
BinaryPresentationsFactory(binaryData, sink, editor).create()
5353
}
5454

5555
else -> null
@@ -69,8 +69,7 @@ object Base64Presentations {
6969

7070
fun create(): Collection<InlayPresentation> {
7171
return element.children.mapNotNull { child ->
72-
val adapter = Base64ValueAdapter(child)
73-
create(adapter)
72+
create(Base64ValueAdapter(child))
7473
}
7574
}
7675

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.redhat.devtools.intellij.kubernetes.editor.inlay.selector
2+
3+
import com.intellij.codeInsight.hints.InlayHintsSink
4+
import com.intellij.codeInsight.hints.presentation.InlayPresentation
5+
import com.intellij.openapi.editor.Editor
6+
import com.intellij.psi.PsiElement
7+
import com.intellij.psi.util.PsiTreeUtil
8+
import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo
9+
import com.redhat.devtools.intellij.kubernetes.editor.util.getKind
10+
import com.redhat.devtools.intellij.kubernetes.editor.util.getMatchExpressions
11+
import com.redhat.devtools.intellij.kubernetes.editor.util.getMatchLabels
12+
import com.redhat.devtools.intellij.kubernetes.editor.util.getMetadataName
13+
import com.redhat.devtools.intellij.kubernetes.editor.util.hasKindAndName
14+
import com.redhat.devtools.intellij.kubernetes.editor.util.isMatchingExpressions
15+
import com.redhat.devtools.intellij.kubernetes.editor.util.isMatchingLabels
16+
import org.jetbrains.yaml.psi.YAMLKeyValue
17+
import org.jetbrains.yaml.psi.YAMLMapping
18+
19+
object SelectorPresentations {
20+
21+
fun create(element: PsiElement, root: PsiElement, info: KubernetesTypeInfo, sink: InlayHintsSink, editor: Editor): Collection<InlayPresentation>? {
22+
if (element is YAMLMapping) {
23+
findMatchingResources(element, root)
24+
}
25+
return emptyList()
26+
}
27+
28+
private fun findMatchingResources(selectorResource: YAMLMapping, root: PsiElement): List<MatchingResource> {
29+
val matchLabels = selectorResource.getMatchLabels()
30+
val matchExpressions = selectorResource.getMatchExpressions()
31+
val allResources = PsiTreeUtil.findChildrenOfType(root, YAMLMapping::class.java)
32+
.filter { it != selectorResource // dont match yourself
33+
it.hasKindAndName() }
34+
35+
return allResources
36+
.filter { resource ->
37+
resource.isMatchingLabels(matchLabels)
38+
&& resource.isMatchingExpressions(matchExpressions)
39+
}
40+
.map { resource ->
41+
val kind = resource.getKind() ?: return emptyList()
42+
val name = resource.getMetadataName() ?: return emptyList()
43+
MatchingResource(kind, name, resource)
44+
}
45+
}
46+
47+
private data class Resource(
48+
val name: String?,
49+
val kind: String?,
50+
val labels: Map<String, String>,
51+
val element: YAMLMapping
52+
) {
53+
fun findMatchLabels(): List<Triple<String, String, YAMLKeyValue>> {
54+
return element.getMatchLabels()?.keyValues?.map { keyValue ->
55+
Triple(keyValue.keyText, keyValue.valueText, keyValue)
56+
}
57+
?: emptyList()
58+
}
59+
}
60+
61+
private data class MatchExpression(
62+
val key: String,
63+
val operator: String,
64+
val values: List<String>,
65+
val element: YAMLMapping
66+
)
67+
68+
private data class MatchingResource(val kind: String, val name: String, val element: PsiElement)
69+
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package com.redhat.devtools.intellij.kubernetes.editor.util
2+
3+
import com.intellij.json.psi.JsonObject
4+
import com.intellij.json.psi.JsonStringLiteral
5+
import com.intellij.openapi.util.text.StringUtil
6+
import org.jetbrains.yaml.psi.YAMLKeyValue
7+
import org.jetbrains.yaml.psi.YAMLMapping
8+
import org.jetbrains.yaml.psi.YAMLSequence
9+
import org.jetbrains.yaml.psi.YAMLSequenceItem
10+
import org.jetbrains.yaml.psi.impl.YAMLMappingImpl
11+
12+
private const val KEY_KIND = "kind"
13+
private const val KEY_METADATA = "metadata"
14+
private const val KEY_NAME = "name"
15+
private const val KEY_LABELS = "labels"
16+
private const val KEY_SPEC = "spec"
17+
private const val KEY_SELECTOR = "selector"
18+
private const val KEY_MATCH_LABELS = "matchLabels"
19+
private const val KEY_MATCH_EXPRESSIONS = "matchExpressions"
20+
// match expression
21+
private const val KEY_KEY = "matchExpressions"
22+
private const val KEY_OPERATOR = "operator"
23+
private enum class OPERATORS { In, NotIn, Exists, DoesNotExist }
24+
private const val KEY_VALUES = "values"
25+
26+
fun YAMLMapping.isMatchingLabels(labels: YAMLMapping?): Boolean {
27+
if (labels == null) {
28+
return false
29+
}
30+
val labels = this.getLabels() ?: return false
31+
32+
for (matchLabel in labels.keyValues) {
33+
val labelName = matchLabel.keyText
34+
val labelValue = (matchLabel.value as? YAMLKeyValue)?.valueText ?: matchLabel.valueText
35+
36+
val resourceLabel = labels.keyValues.find { it.keyText == labelName }
37+
if (resourceLabel == null || resourceLabel.valueText != labelValue) {
38+
return false
39+
}
40+
}
41+
42+
return true
43+
}
44+
45+
fun YAMLMapping.isMatchingExpressions(expressions: YAMLSequence?): Boolean {
46+
if (expressions == null) {
47+
return true
48+
}
49+
return expressions.items.all { expression ->
50+
this.isMatchingExpression(expression)
51+
}
52+
}
53+
54+
fun YAMLMapping.isMatchingExpression(item: YAMLSequenceItem?,): Boolean {
55+
val labels = this.getLabels() ?: return false
56+
57+
if (item !is YAMLMapping) return true // ignore invalid yaml
58+
59+
val key = item.getKeyValueByKey(KEY_KEY)?.value ?: return true // ignore expression without key
60+
val operator = item.getKeyValueByKey(KEY_OPERATOR)?.value ?: return true // ignore expression without operator
61+
val values = item.getKeyValueByKey(KEY_VALUES)?.value as? YAMLSequence
62+
63+
val resourceLabel = labels.keyValues.find { it.keyText == key.text }
64+
65+
return when (operator.text) {
66+
OPERATORS.In.name -> {
67+
resourceLabel == null
68+
|| values == null
69+
|| values.items.all {
70+
StringUtil.unquoteString(it.text) == StringUtil.unquoteString(resourceLabel.valueText)
71+
}
72+
}
73+
74+
OPERATORS.NotIn.name -> {
75+
resourceLabel != null
76+
&& values != null
77+
&& values.items.none {
78+
StringUtil.unquoteString(it.text) == StringUtil.unquoteString(resourceLabel.valueText)
79+
}
80+
}
81+
82+
OPERATORS.Exists.name -> {
83+
resourceLabel != null
84+
}
85+
86+
OPERATORS.DoesNotExist.name -> {
87+
resourceLabel == null
88+
}
89+
90+
else -> {
91+
false
92+
}
93+
}
94+
}
95+
96+
fun YAMLMapping.getMatchLabels(): YAMLMapping? {
97+
val selector = this.getSelector() ?: return null
98+
val matchLabels = selector.getKeyValueByKey(KEY_MATCH_LABELS)
99+
return matchLabels?.value as YAMLMapping?
100+
?: selector
101+
}
102+
103+
fun YAMLMapping.getSelector(): YAMLMapping? {
104+
return (this.getKeyValueByKey(KEY_SPEC)?.value as? YAMLMappingImpl)
105+
?.getKeyValueByKey(KEY_SELECTOR)?.value as? YAMLMapping
106+
}
107+
108+
fun YAMLMapping.getMatchExpressions(): YAMLSequence? {
109+
return ((this.getKeyValueByKey(KEY_SPEC) as? YAMLMapping)
110+
?.getKeyValueByKey(KEY_SELECTOR) as? YAMLMapping)
111+
?.getKeyValueByKey(KEY_MATCH_EXPRESSIONS) as? YAMLSequence?
112+
}
113+
114+
fun YAMLMapping.hasKindAndName(): Boolean {
115+
val kind = this.getKeyValueByKey(KEY_KIND)?.valueText
116+
val metadata = this.getMetadata()
117+
val name = metadata?.getKeyValueByKey(KEY_NAME)?.valueText
118+
return !kind.isNullOrEmpty() && !name.isNullOrEmpty()
119+
}
120+
121+
fun YAMLMapping.getKind(): String? {
122+
return this.getKeyValueByKey(KEY_KIND)
123+
?.valueText
124+
}
125+
126+
fun YAMLMapping.getMetadataName(): String? {
127+
return this.getMetadata()
128+
?.getKeyValueByKey(KEY_NAME)
129+
?.valueText
130+
}
131+
132+
fun YAMLMapping.getLabels(): YAMLMapping? {
133+
return this.getMetadata()
134+
?.getKeyValueByKey(KEY_LABELS)
135+
?.value as? YAMLMapping
136+
}
137+
138+
fun YAMLMapping.getMetadata(): YAMLMapping? {
139+
return this.getKeyValueByKey(KEY_METADATA)
140+
?.value as? YAMLMapping
141+
}
142+
143+
fun JsonObject.hasKindAndName(): Boolean {
144+
val kind = this.findProperty(KEY_KIND)?.value as? JsonStringLiteral
145+
val metadata = this.getMetadata()
146+
val name = metadata?.findProperty(KEY_NAME)?.value as? JsonStringLiteral
147+
return kind != null && name != null
148+
}
149+
150+
fun JsonObject.getKind(): String? {
151+
val kind = this.findProperty(KEY_KIND)?.value as? JsonStringLiteral
152+
return kind?.value
153+
}
154+
155+
fun JsonObject.getMetadataName(): String? {
156+
val metadata = this.getMetadata()
157+
val name = metadata?.findProperty(KEY_NAME)?.value as? JsonStringLiteral
158+
return name?.value
159+
}
160+
161+
fun JsonObject.getLabels(): JsonObject? {
162+
val metadata = this.getMetadata()
163+
return metadata
164+
}
165+
166+
fun JsonObject.getMetadata(): JsonObject? {
167+
return this.findProperty(KEY_METADATA)?.value as? JsonObject
168+
}
169+
170+
fun JsonObject.getMatchLabels(): JsonObject? {
171+
return ((this.findProperty(KEY_SPEC) as? JsonObject)
172+
?.findProperty(KEY_SELECTOR) as? JsonObject)
173+
?.findProperty(KEY_MATCH_LABELS) as? JsonObject
174+
}
175+

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt

+10
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
******************************************************************************/
1111
package com.redhat.devtools.intellij.kubernetes.editor.util
1212

13+
import com.intellij.json.JsonFileType
1314
import com.intellij.json.psi.JsonElement
1415
import com.intellij.json.psi.JsonElementGenerator
1516
import com.intellij.json.psi.JsonFile
1617
import com.intellij.json.psi.JsonProperty
1718
import com.intellij.json.psi.JsonValue
1819
import com.intellij.openapi.application.ReadAction
1920
import com.intellij.openapi.fileEditor.FileEditor
21+
import com.intellij.openapi.fileTypes.LanguageFileType
2022
import com.intellij.openapi.project.Project
2123
import com.intellij.openapi.util.text.StringUtil
2224
import com.intellij.openapi.util.text.Strings
@@ -28,6 +30,7 @@ import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo
2830
import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo
2931
import com.redhat.devtools.intellij.kubernetes.editor.ResourceEditor
3032
import org.jetbrains.yaml.YAMLElementGenerator
33+
import org.jetbrains.yaml.YAMLFileType
3134
import org.jetbrains.yaml.psi.YAMLDocument
3235
import org.jetbrains.yaml.psi.YAMLKeyValue
3336
import org.jetbrains.yaml.psi.YAMLMapping
@@ -274,3 +277,10 @@ fun getExistingResourceEditor(file: VirtualFile?): ResourceEditor? {
274277
return file?.getUserData(ResourceEditor.KEY_RESOURCE_EDITOR)
275278
}
276279

280+
fun isYAML(fileType: LanguageFileType?): Boolean {
281+
return YAMLFileType.YML == fileType
282+
}
283+
284+
fun isJSON(fileType: LanguageFileType?): Boolean {
285+
return JsonFileType.INSTANCE == fileType
286+
}

0 commit comments

Comments
 (0)