1
+ /* ******************************************************************************
2
+ * Copyright (c) 2024 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
+ @file:Suppress(" UnstableApiUsage" )
12
+
13
+ package com.redhat.devtools.intellij.kubernetes.editor.inlay
14
+
15
+ import com.intellij.codeInsight.hints.ChangeListener
16
+ import com.intellij.codeInsight.hints.FactoryInlayHintsCollector
17
+ import com.intellij.codeInsight.hints.ImmediateConfigurable
18
+ import com.intellij.codeInsight.hints.InlayHintsCollector
19
+ import com.intellij.codeInsight.hints.InlayHintsProvider
20
+ import com.intellij.codeInsight.hints.InlayHintsSink
21
+ import com.intellij.codeInsight.hints.NoSettings
22
+ import com.intellij.codeInsight.hints.SettingsKey
23
+ import com.intellij.codeInsight.hints.presentation.InlayPresentation
24
+ import com.intellij.codeInsight.hints.presentation.PresentationFactory
25
+ import com.intellij.openapi.command.WriteCommandAction
26
+ import com.intellij.openapi.editor.Editor
27
+ import com.intellij.openapi.editor.impl.EditorImpl
28
+ import com.intellij.openapi.project.Project
29
+ import com.intellij.psi.PsiElement
30
+ import com.intellij.psi.PsiFile
31
+ import com.intellij.ui.dsl.builder.panel
32
+ import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo
33
+ import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon
34
+ import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64
35
+ import com.redhat.devtools.intellij.kubernetes.editor.util.encodeBase64
36
+ import com.redhat.devtools.intellij.kubernetes.editor.util.getBinaryData
37
+ import com.redhat.devtools.intellij.kubernetes.editor.util.getContent
38
+ import com.redhat.devtools.intellij.kubernetes.editor.util.getData
39
+ import com.redhat.devtools.intellij.kubernetes.editor.util.getStartOffset
40
+ import com.redhat.devtools.intellij.kubernetes.editor.util.getValue
41
+ import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource
42
+ import com.redhat.devtools.intellij.kubernetes.editor.util.setValue
43
+ import org.jetbrains.concurrency.runAsync
44
+ import java.awt.event.MouseEvent
45
+ import javax.swing.JComponent
46
+
47
+
48
+ internal class Base64ValueInlayHintsProvider : InlayHintsProvider <NoSettings > {
49
+
50
+ override val key: SettingsKey <NoSettings > = SettingsKey (" KubernetesResource.hints" )
51
+ override val name: String = " Kubernetes"
52
+ override val previewText: String = " Preview"
53
+
54
+ override fun createSettings (): NoSettings {
55
+ return NoSettings ()
56
+ }
57
+
58
+ override fun createConfigurable (settings : NoSettings ): ImmediateConfigurable {
59
+ return object : ImmediateConfigurable {
60
+ override fun createComponent (listener : ChangeListener ): JComponent = panel {}
61
+
62
+ override val mainCheckboxText: String = " Show hints for:"
63
+
64
+ override val cases: List <ImmediateConfigurable .Case > = emptyList()
65
+ }
66
+ }
67
+
68
+ override fun getCollectorFor (file : PsiFile , editor : Editor , settings : NoSettings , sink : InlayHintsSink ): InlayHintsCollector ? {
69
+ val info = KubernetesResourceInfo .extractMeta(file) ? : return null
70
+ if (! isKubernetesResource(" Secret" , info)
71
+ && ! isKubernetesResource(" ConfigMap" , info)) {
72
+ return null
73
+ }
74
+ return Collector (editor, info)
75
+ }
76
+
77
+ private class Collector (editor : Editor , private val info : KubernetesResourceInfo ) : FactoryInlayHintsCollector(editor) {
78
+
79
+ override fun collect (element : PsiElement , editor : Editor , sink : InlayHintsSink ): Boolean {
80
+ if (element !is PsiFile
81
+ || ! element.isValid) {
82
+ return true
83
+ }
84
+ val content = getContent(element) ? : return true
85
+ val base64Element = getBase64Element(info, content) ? : return true
86
+ createPresentations(base64Element, editor, sink)
87
+ return false
88
+ }
89
+
90
+ private fun getBase64Element (info : KubernetesResourceInfo ? , content : PsiElement ): PsiElement ? {
91
+ return when {
92
+ isKubernetesResource(" Secret" , info) ->
93
+ // all entries in 'data' are base64 encoded
94
+ getData(content)
95
+
96
+ isKubernetesResource(" ConfigMap" , info) ->
97
+ // all entries in 'binaryData' are base64 encoded
98
+ getBinaryData(content)
99
+
100
+ else -> null
101
+ }
102
+ }
103
+
104
+ private fun createPresentations (element : PsiElement , editor : Editor , sink : InlayHintsSink ): Collection <InlayPresentation > {
105
+ return element.children.mapNotNull { child ->
106
+ val adapter = Base64ValueAdapter (child)
107
+ val onClick = StringInputBalloon (onValidValue(adapter::set, editor.project), editor)::show
108
+ createPresentation(onClick, adapter, editor, sink)
109
+ }
110
+ }
111
+
112
+ private fun createPresentation (onClick : (event: MouseEvent ) -> Unit , adapter : Base64ValueAdapter , editor : Editor , sink : InlayHintsSink ): InlayPresentation ? {
113
+ val offset = adapter.getStartOffset() ? : return null
114
+ val decoded = adapter.getDecoded() ? : return null
115
+ val presentation = createPresentation(decoded, onClick, editor)
116
+ sink.addInlineElement(offset, false , presentation, false )
117
+ return presentation
118
+ }
119
+
120
+ private fun createPresentation (decoded : String , onClick : (event: MouseEvent ) -> Unit , editor : Editor ): InlayPresentation {
121
+ val factory = PresentationFactory (editor as EditorImpl )
122
+ val textPresentation = factory.smallText(decoded)
123
+ val hoverPresentation = factory.referenceOnHover(textPresentation) { event, translated ->
124
+ onClick.invoke(event)
125
+ }
126
+ val tooltipPresentation = factory.withTooltip(" Click to change value" , hoverPresentation)
127
+ val roundPresentation = factory.roundWithBackground(tooltipPresentation)
128
+ return roundPresentation
129
+ }
130
+
131
+ fun onValidValue (setter : (value: String , project: Project ? ) -> Unit , project : Project ? ): (value: String ) -> Unit {
132
+ return { value ->
133
+ runAsync {
134
+ WriteCommandAction .runWriteCommandAction(project) {
135
+ setter.invoke(value, project)
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ private class Base64ValueAdapter (private val element : PsiElement ) {
143
+
144
+ private companion object {
145
+ private val CONTENT_REGEX = Regex (" [^ |\n ]*" , setOf (RegexOption .MULTILINE ))
146
+ private val PIPE_CHARACTER = ' |'
147
+ }
148
+
149
+ fun set (value : String , project : Project ? ) {
150
+ val encoded = encodeBase64(value) ? : return
151
+ setValue(encoded, element, project)
152
+ }
153
+
154
+ fun get (): String? {
155
+ return getValue(element)
156
+ }
157
+
158
+ fun getDecoded (): String? {
159
+ val value = getValue(element) ? : return null
160
+ val content = CONTENT_REGEX
161
+ .findAll(value)
162
+ .filter { matchResult -> matchResult.value.isNotBlank() }
163
+ .map { matchResult -> matchResult.value }
164
+ .joinToString(separator = " " )
165
+ return decodeBase64(content)
166
+ }
167
+
168
+ fun getStartOffset (): Int? {
169
+ return getStartOffset(element)
170
+ }
171
+ }
172
+ }
0 commit comments