Skip to content

Commit c1c2c85

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 c1c2c85

30 files changed

+3684
-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,168 @@
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.openapi.fileEditor.FileEditor
19+
import com.intellij.psi.PsiElement
20+
import com.intellij.ui.IconManager
21+
import com.intellij.ui.awt.RelativePoint
22+
import com.redhat.devtools.intellij.kubernetes.editor.util.getKey
23+
import com.redhat.devtools.intellij.kubernetes.editor.util.getLabels
24+
import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector
25+
import com.redhat.devtools.intellij.kubernetes.editor.util.getTemplate
26+
import com.redhat.devtools.intellij.kubernetes.editor.util.hasTemplate
27+
import com.redhat.devtools.intellij.kubernetes.usage.LabelsFilter
28+
import com.redhat.devtools.intellij.kubernetes.usage.SelectorsFilter
29+
import java.awt.event.MouseEvent
30+
import javax.swing.Icon
31+
32+
33+
object SelectorPresentations {
34+
35+
private val selectorIcon = IconManager.getInstance().getIcon("icons/selector.svg", javaClass)
36+
private val labelIcon = IconManager.getInstance().getIcon("icons/label.svg", javaClass)
37+
38+
fun createForSelector(
39+
element: PsiElement,
40+
allElements: List<PsiElement>,
41+
sink: InlayHintsSink,
42+
editor: Editor,
43+
factory: PresentationFactory
44+
) {
45+
val filter = LabelsFilter(element)
46+
val matchingElements = allElements
47+
.filter(filter::isAccepted)
48+
val selectorAttribute = element.getSelector()?.parent
49+
?: return
50+
51+
create(
52+
selectorAttribute,
53+
"${matchingElements.size} matching",
54+
"Click to see matching labels",
55+
selectorIcon,
56+
editor,
57+
sink,
58+
factory
59+
)
60+
}
61+
62+
fun createForAllLabels(
63+
element: PsiElement,
64+
allElements: List<PsiElement>,
65+
sink: InlayHintsSink,
66+
editor: Editor,
67+
factory: PresentationFactory
68+
) {
69+
createForLabels(element, element.getLabels(), allElements, sink, editor, factory)
70+
if (element.hasTemplate()) {
71+
createForLabels(element, element.getTemplate()?.getLabels(), allElements, sink, editor, factory)
72+
}
73+
}
74+
75+
private fun createForLabels(
76+
resource: PsiElement,
77+
labels: PsiElement?,
78+
allElements: List<PsiElement>,
79+
sink: InlayHintsSink,
80+
editor: Editor,
81+
factory: PresentationFactory
82+
) {
83+
val labelsAttribute = labels?.parent
84+
?: return
85+
val filter = SelectorsFilter(resource)
86+
val matchingElements = allElements
87+
.filter(filter::isAccepted)
88+
create(
89+
labelsAttribute,
90+
"${matchingElements.size} matching",
91+
"Click to see matching selectors",
92+
labelIcon,
93+
editor,
94+
sink,
95+
factory
96+
)
97+
}
98+
99+
private fun create(
100+
element: PsiElement,
101+
text: String,
102+
toolTip: String,
103+
icon: Icon,
104+
editor: Editor,
105+
sink: InlayHintsSink,
106+
factory: PresentationFactory
107+
) {
108+
val offset = element.getKey()?.textRange?.endOffset // to the right of the key
109+
?: return
110+
111+
val textPresentation = createText(element, text, toolTip, editor, factory)
112+
sink.addInlineElement(offset, true, textPresentation, true)
113+
114+
val iconPresentation = createIcon(element, factory, editor, icon)
115+
sink.addInlineElement(offset, true, iconPresentation, true)
116+
}
117+
118+
private fun createText(
119+
element: PsiElement,
120+
text: String,
121+
toolTip: String,
122+
editor: Editor,
123+
factory: PresentationFactory
124+
): InlayPresentation {
125+
return factory.withTooltip(
126+
toolTip,
127+
factory.referenceOnHover(
128+
factory.roundWithBackground(
129+
factory.text(
130+
text
131+
)
132+
),
133+
onClick(editor, element)
134+
)
135+
)
136+
}
137+
138+
private fun createIcon(
139+
selector: PsiElement,
140+
factory: PresentationFactory,
141+
editor: Editor,
142+
icon: Icon
143+
): InlayPresentation {
144+
val iconPresentation = factory.referenceOnHover(
145+
factory.roundWithBackground(
146+
factory.smallScaledIcon(icon)
147+
),
148+
onClick(editor, selector)
149+
)
150+
return iconPresentation
151+
}
152+
153+
private fun onClick(editor: Editor, hintedKeyValue: PsiElement):
154+
(event: MouseEvent, point: java.awt.Point) -> Unit {
155+
156+
return { event, point ->
157+
val project = editor.project
158+
if (project != null) {
159+
ShowUsagesAction.startFindUsages(hintedKeyValue, RelativePoint(event), editor)
160+
//ShowUsagesDispatcher.runWithCustomScope(project, hintedKeyValue)
161+
//ShowUsagesDispatcher.findUsageManager(project, hintedKeyValue, editor)
162+
}
163+
}
164+
}
165+
166+
167+
}
168+

0 commit comments

Comments
 (0)