Skip to content

Commit 8ef1541

Browse files
committed
feat: hint ref'd by matchLabels/-Expressions & list them in editor (#642)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent 899bcc2 commit 8ef1541

30 files changed

+3682
-332
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,6 @@ open class ResourceEditorFactory protected constructor(
171171

172172
/* for testing purposes */
173173
protected open fun getTelemetryMessageBuilder(): TelemetryMessageBuilder {
174-
return TelemetryService.instance;
174+
return TelemetryService.instance
175175
}
176176
}

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

+26-13
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@ import com.intellij.codeInsight.hints.InlayHintsProvider
2020
import com.intellij.codeInsight.hints.InlayHintsSink
2121
import com.intellij.codeInsight.hints.NoSettings
2222
import com.intellij.codeInsight.hints.SettingsKey
23+
import com.intellij.codeInsight.hints.presentation.PresentationFactory
2324
import com.intellij.json.psi.JsonFile
24-
import com.intellij.openapi.application.ApplicationManager
2525
import com.intellij.openapi.application.ReadAction
2626
import com.intellij.openapi.editor.Editor
2727
import com.intellij.psi.PsiElement
2828
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
33+
import com.redhat.devtools.intellij.kubernetes.editor.util.PsiElements
3234
import org.jetbrains.yaml.psi.YAMLFile
3335
import javax.swing.JComponent
3436

@@ -65,34 +67,45 @@ internal class ResourceEditorInlayHintsProvider : InlayHintsProvider<NoSettings>
6567
}
6668
return when(element) {
6769
is YAMLFile -> {
68-
create(element, sink, editor)
70+
create(element, sink, editor, factory)
6971
false
7072
}
7173
is JsonFile -> {
72-
create(element, sink, editor)
74+
create(element, sink, editor, factory)
7375
false
7476
}
7577
else -> true
7678
}
7779
}
7880

79-
private fun create(file: YAMLFile, sink: InlayHintsSink, editor: Editor) {
81+
private fun create(file: YAMLFile, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) {
8082
return ReadAction.run<Exception> {
81-
file.documents.forEach { document ->
82-
val info = KubernetesTypeInfo.create(document) ?: return@forEach
83-
val element = document.topLevelValue ?: return@forEach
84-
Base64Presentations.create(element, info, sink, editor)?.create()
85-
}
83+
file.documents
84+
.mapNotNull { document -> document.topLevelValue }
85+
.forEach { element ->
86+
createPresentations(element, sink, editor, factory)
87+
}
8688
}
8789
}
8890

89-
private fun create(file: JsonFile, sink: InlayHintsSink, editor: Editor) {
91+
private fun create(file: JsonFile, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) {
9092
return ReadAction.run<Exception> {
91-
val info = KubernetesTypeInfo.create(file) ?: return@run
92-
val element = file.topLevelValue ?: return@run
93-
Base64Presentations.create(element, info, sink, editor)?.create()
93+
file.allTopLevelValues.forEach { element ->
94+
createPresentations(element, sink, editor, factory)
95+
}
9496
}
9597
}
9698

99+
private fun createPresentations(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) {
100+
val info = KubernetesTypeInfo.create(element) ?: return
101+
Base64Presentations.create(element, info, sink, editor, factory)
102+
103+
val fileType = editor.virtualFile?.fileType ?: return
104+
val project = editor.project ?: return
105+
//PsiElements.getAll(fileType, project)
106+
val allElements = PsiElements.getAllNoExclusions(fileType, project)
107+
SelectorPresentations.createForSelector(element, allElements, sink, editor, factory)
108+
SelectorPresentations.createForAllLabels(element, allElements, sink, editor, factory)
109+
}
97110
}
98111
}

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

+40-25
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,41 @@ 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? {
44-
return when {
43+
fun create(
44+
element: PsiElement,
45+
info: KubernetesTypeInfo,
46+
sink: InlayHintsSink,
47+
editor: Editor,
48+
factory: PresentationFactory,
49+
/* for testing purposes */
50+
stringPresentationFactory: (element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit
51+
= { element, sink, editor, factory ->
52+
StringPresentationsFactory(element, sink, editor, factory).create()
53+
},
54+
/* for testing purposes */
55+
binaryPresentationFactory: (element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit
56+
= { element, sink, editor, factory ->
57+
BinaryPresentationsFactory(element, sink, editor, factory).create()
58+
},
59+
) {
60+
when {
4561
isKubernetesResource(SECRET_RESOURCE_KIND, info) -> {
46-
val data = getDataValue(element) ?: return null
47-
StringPresentationsFactory(data, sink, editor)
62+
val data = element.getDataValue() ?: return
63+
stringPresentationFactory.invoke(data, sink, editor, factory)
4864
}
4965

5066
isKubernetesResource(CONFIGMAP_RESOURCE_KIND, info) -> {
51-
val binaryData = getBinaryData(element) ?: return null
52-
BinaryPresentationsFactory(binaryData, sink, editor)
67+
val binaryData = element.getBinaryData() ?: return
68+
binaryPresentationFactory.invoke(binaryData, sink, editor, factory)
5369
}
54-
55-
else -> null
5670
}
5771
}
5872

5973
abstract class InlayPresentationsFactory(
6074
private val element: PsiElement,
6175
protected val sink: InlayHintsSink,
62-
protected val editor: Editor
76+
protected val editor: Editor,
77+
protected val factory: PresentationFactory
6378
) {
6479

6580
protected companion object {
@@ -69,17 +84,15 @@ object Base64Presentations {
6984

7085
fun create(): Collection<InlayPresentation> {
7186
return element.children.mapNotNull { child ->
72-
val adapter = Base64ValueAdapter(child)
73-
create(adapter)
87+
create(Base64ValueAdapter(child))
7488
}
7589
}
7690

7791
protected abstract fun create(adapter: Base64ValueAdapter): InlayPresentation?
78-
7992
}
8093

81-
class StringPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor)
82-
: InlayPresentationsFactory(element, sink, editor) {
94+
class StringPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory)
95+
: InlayPresentationsFactory(element, sink, editor, factory) {
8396

8497
override fun create(adapter: Base64ValueAdapter): InlayPresentation? {
8598
val decoded = adapter.getDecoded() ?: return null
@@ -89,20 +102,22 @@ object Base64Presentations {
89102
onValidValue(adapter::set, editor.project),
90103
editor
91104
)::show
92-
val presentation = create(decoded, onClick, editor) ?: return null
105+
val presentation = create(decoded, onClick, factory) ?: return null
93106
sink.addInlineElement(offset, false, presentation, false)
94107
return presentation
95108
}
96109

97-
private fun create(text: String, onClick: (event: MouseEvent) -> Unit, editor: Editor): InlayPresentation? {
98-
val factory = PresentationFactory(editor)
110+
private fun create(text: String, onClick: (event: MouseEvent) -> Unit, factory: PresentationFactory): InlayPresentation? {
99111
val trimmed = trimWithEllipsis(text, INLAY_HINT_MAX_WIDTH) ?: return null
100-
val textPresentation = factory.smallText(trimmed)
101-
val hoverPresentation = factory.referenceOnHover(textPresentation) { event, _ ->
102-
onClick.invoke(event)
103-
}
104-
val tooltipPresentation = factory.withTooltip("Click to change value", hoverPresentation)
105-
return factory.roundWithBackground(tooltipPresentation)
112+
return factory.roundWithBackground(
113+
factory.withTooltip(
114+
"Click to change value",
115+
factory.referenceOnHover(
116+
factory.smallText(trimmed)) { event, _ ->
117+
onClick.invoke(event)
118+
}
119+
)
120+
)
106121
}
107122

108123
private fun onValidValue(setter: (value: String, wrapAt: Int) -> Unit, project: Project?)
@@ -118,8 +133,8 @@ object Base64Presentations {
118133

119134
}
120135

121-
class BinaryPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor)
122-
: InlayPresentationsFactory(element, sink, editor) {
136+
class BinaryPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory)
137+
: InlayPresentationsFactory(element, sink, editor, factory) {
123138

124139
override fun create(adapter: Base64ValueAdapter): InlayPresentation? {
125140
val decoded = adapter.getDecodedBytes() ?: return null

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
******************************************************************************/
1111
package com.redhat.devtools.intellij.kubernetes.editor.inlay.base64
1212

13+
import com.intellij.json.psi.JsonProperty
1314
import com.intellij.psi.PsiElement
1415
import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64
1516
import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64ToBytes
1617
import com.redhat.devtools.intellij.kubernetes.editor.util.encodeBase64
1718
import com.redhat.devtools.intellij.kubernetes.editor.util.getValue
1819
import com.redhat.devtools.intellij.kubernetes.editor.util.setValue
20+
import org.jetbrains.yaml.psi.YAMLKeyValue
1921

2022
class Base64ValueAdapter(private val element: PsiElement) {
2123

@@ -80,6 +82,10 @@ class Base64ValueAdapter(private val element: PsiElement) {
8082
}
8183

8284
fun getStartOffset(): Int? {
83-
return com.redhat.devtools.intellij.kubernetes.editor.util.getStartOffset(element)
85+
return when(element) {
86+
is YAMLKeyValue -> element.value?.textRange?.startOffset
87+
is JsonProperty -> element.value?.textRange?.startOffset
88+
else -> null
89+
}
8490
}
8591
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.editor.inlay.selector
12+
13+
import com.intellij.codeInsight.hints.InlayHintsSink
14+
import com.intellij.codeInsight.hints.presentation.InlayPresentation
15+
import com.intellij.codeInsight.hints.presentation.PresentationFactory
16+
import com.intellij.find.actions.ShowUsagesAction
17+
import com.intellij.openapi.editor.Editor
18+
import com.intellij.psi.PsiElement
19+
import com.intellij.ui.IconManager
20+
import com.intellij.ui.awt.RelativePoint
21+
import com.redhat.devtools.intellij.kubernetes.editor.util.getKey
22+
import com.redhat.devtools.intellij.kubernetes.editor.util.getLabels
23+
import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector
24+
import com.redhat.devtools.intellij.kubernetes.editor.util.getTemplate
25+
import com.redhat.devtools.intellij.kubernetes.editor.util.hasTemplate
26+
import com.redhat.devtools.intellij.kubernetes.usage.LabelsFilter
27+
import com.redhat.devtools.intellij.kubernetes.usage.SelectorsFilter
28+
import java.awt.event.MouseEvent
29+
import javax.swing.Icon
30+
31+
32+
object SelectorPresentations {
33+
34+
private val selectorIcon = IconManager.getInstance().getIcon("icons/selector.svg", javaClass)
35+
private val labelIcon = IconManager.getInstance().getIcon("icons/label.svg", javaClass)
36+
37+
fun createForSelector(
38+
element: PsiElement,
39+
allElements: List<PsiElement>,
40+
sink: InlayHintsSink,
41+
editor: Editor,
42+
factory: PresentationFactory
43+
) {
44+
val filter = LabelsFilter(element)
45+
val matchingElements = allElements
46+
.filter(filter::isAccepted)
47+
val selectorAttribute = element.getSelector()?.parent
48+
?: return
49+
50+
create(
51+
selectorAttribute,
52+
"${matchingElements.size} matching",
53+
"Click to see matching labels",
54+
selectorIcon,
55+
editor,
56+
sink,
57+
factory
58+
)
59+
}
60+
61+
fun createForAllLabels(
62+
element: PsiElement,
63+
allElements: List<PsiElement>,
64+
sink: InlayHintsSink,
65+
editor: Editor,
66+
factory: PresentationFactory
67+
) {
68+
createForLabels(element, element.getLabels(), allElements, sink, editor, factory)
69+
if (element.hasTemplate()) {
70+
createForLabels(element, element.getTemplate()?.getLabels(), allElements, sink, editor, factory)
71+
}
72+
}
73+
74+
private fun createForLabels(
75+
resource: PsiElement,
76+
labels: PsiElement?,
77+
allElements: List<PsiElement>,
78+
sink: InlayHintsSink,
79+
editor: Editor,
80+
factory: PresentationFactory
81+
) {
82+
val labelsAttribute = labels?.parent
83+
?: return
84+
val filter = SelectorsFilter(resource)
85+
val matchingElements = allElements
86+
.filter(filter::isAccepted)
87+
create(
88+
labelsAttribute,
89+
"${matchingElements.size} matching",
90+
"Click to see matching selectors",
91+
labelIcon,
92+
editor,
93+
sink,
94+
factory
95+
)
96+
}
97+
98+
private fun create(
99+
element: PsiElement,
100+
text: String,
101+
toolTip: String,
102+
icon: Icon,
103+
editor: Editor,
104+
sink: InlayHintsSink,
105+
factory: PresentationFactory
106+
) {
107+
val offset = element.getKey()?.textRange?.endOffset // to the right of the key
108+
?: return
109+
110+
val textPresentation = createText(element, text, toolTip, editor, factory)
111+
sink.addInlineElement(offset, true, textPresentation, true)
112+
113+
val iconPresentation = createIcon(element, factory, editor, icon)
114+
sink.addInlineElement(offset, true, iconPresentation, true)
115+
}
116+
117+
private fun createText(
118+
element: PsiElement,
119+
text: String,
120+
toolTip: String,
121+
editor: Editor,
122+
factory: PresentationFactory
123+
): InlayPresentation {
124+
return factory.withTooltip(
125+
toolTip,
126+
factory.referenceOnHover(
127+
factory.roundWithBackground(
128+
factory.text(
129+
text
130+
)
131+
),
132+
onClick(editor, element)
133+
)
134+
)
135+
}
136+
137+
private fun createIcon(
138+
selector: PsiElement,
139+
factory: PresentationFactory,
140+
editor: Editor,
141+
icon: Icon
142+
): InlayPresentation {
143+
val iconPresentation = factory.referenceOnHover(
144+
factory.roundWithBackground(
145+
factory.smallScaledIcon(icon)
146+
),
147+
onClick(editor, selector)
148+
)
149+
return iconPresentation
150+
}
151+
152+
private fun onClick(editor: Editor, hintedKeyValue: PsiElement):
153+
(event: MouseEvent, point: java.awt.Point) -> Unit {
154+
155+
return { event, point ->
156+
val project = editor.project
157+
if (project != null) {
158+
ShowUsagesAction.startFindUsages(hintedKeyValue, RelativePoint(event), editor)
159+
//ShowUsagesDispatcher.runWithCustomScope(project, hintedKeyValue)
160+
//ShowUsagesDispatcher.findUsageManager(project, hintedKeyValue, editor)
161+
}
162+
}
163+
}
164+
165+
166+
}
167+

0 commit comments

Comments
 (0)