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 com.redhat.devtools.intellij.kubernetes.model.util.trimWithEllipsis
44
+ import org.jetbrains.concurrency.runAsync
45
+ import java.awt.event.MouseEvent
46
+ import javax.swing.JComponent
47
+
48
+
49
+ internal class Base64ValueInlayHintsProvider : InlayHintsProvider <NoSettings > {
50
+
51
+ companion object {
52
+ private const val SECRET_RESOURCE_KIND = " Secret"
53
+ private const val CONFIGMAP_RESOURCE_KIND = " ConfigMap"
54
+ }
55
+
56
+ override val key: SettingsKey <NoSettings > = SettingsKey (" KubernetesResource.hints" )
57
+ override val name: String = " Kubernetes"
58
+ override val previewText: String = " Preview"
59
+
60
+ override fun createSettings (): NoSettings {
61
+ return NoSettings ()
62
+ }
63
+
64
+ override fun createConfigurable (settings : NoSettings ): ImmediateConfigurable {
65
+ return object : ImmediateConfigurable {
66
+ override fun createComponent (listener : ChangeListener ): JComponent = panel {}
67
+
68
+ override val mainCheckboxText: String = " Show hints for:"
69
+
70
+ override val cases: List <ImmediateConfigurable .Case > = emptyList()
71
+ }
72
+ }
73
+
74
+ override fun getCollectorFor (file : PsiFile , editor : Editor , settings : NoSettings , sink : InlayHintsSink ): InlayHintsCollector ? {
75
+ val info = KubernetesResourceInfo .extractMeta(file) ? : return null
76
+ if (! isKubernetesResource(SECRET_RESOURCE_KIND , info)
77
+ && ! isKubernetesResource(CONFIGMAP_RESOURCE_KIND , info)) {
78
+ return null
79
+ }
80
+ return Collector (editor, info)
81
+ }
82
+
83
+ private class Collector (editor : Editor , private val info : KubernetesResourceInfo ) : FactoryInlayHintsCollector(editor) {
84
+
85
+ override fun collect (element : PsiElement , editor : Editor , sink : InlayHintsSink ): Boolean {
86
+ if (element !is PsiFile
87
+ || ! element.isValid) {
88
+ return true
89
+ }
90
+ val content = getContent(element) ? : return true
91
+ val base64Element = getBase64Element(info, content) ? : return true
92
+ createPresentations(base64Element, editor, sink)
93
+ return false
94
+ }
95
+
96
+ private fun getBase64Element (info : KubernetesResourceInfo ? , content : PsiElement ): PsiElement ? {
97
+ return when {
98
+ isKubernetesResource(SECRET_RESOURCE_KIND , info) ->
99
+ // all entries in 'data' are base64 encoded
100
+ getData(content)
101
+
102
+ isKubernetesResource(CONFIGMAP_RESOURCE_KIND , info) ->
103
+ // all entries in 'binaryData' are base64 encoded
104
+ getBinaryData(content)
105
+
106
+ else -> null
107
+ }
108
+ }
109
+
110
+ private fun createPresentations (element : PsiElement , editor : Editor , sink : InlayHintsSink ): Collection <InlayPresentation > {
111
+ return element.children.mapNotNull { child ->
112
+ val adapter = Base64ValueAdapter (child)
113
+ val decoded = adapter.getDecoded()
114
+ if (decoded != null ) {
115
+ val onClick = StringInputBalloon (
116
+ decoded,
117
+ onValidValue(adapter::set, editor.project),
118
+ editor
119
+ )::show
120
+ createPresentation(onClick, adapter, editor, sink)
121
+ } else {
122
+ null
123
+ }
124
+ }
125
+ }
126
+
127
+ private fun createPresentation (onClick : (event: MouseEvent ) -> Unit , adapter : Base64ValueAdapter , editor : Editor , sink : InlayHintsSink ): InlayPresentation ? {
128
+ val offset = adapter.getStartOffset() ? : return null
129
+ val decoded = adapter.getDecoded() ? : return null
130
+ val presentation = createPresentation(decoded, onClick, editor) ? : return null
131
+ sink.addInlineElement(offset, false , presentation, false )
132
+ return presentation
133
+ }
134
+
135
+ private fun createPresentation (text : String , onClick : (event: MouseEvent ) -> Unit , editor : Editor ): InlayPresentation ? {
136
+ val factory = PresentationFactory (editor as EditorImpl )
137
+ val trimmed = trimWithEllipsis(text, 50 ) ? : return null
138
+ val textPresentation = factory.smallText(trimmed)
139
+ val hoverPresentation = factory.referenceOnHover(textPresentation) { event, translated ->
140
+ onClick.invoke(event)
141
+ }
142
+ val tooltipPresentation = factory.withTooltip(" Click to change value" , hoverPresentation)
143
+ val roundPresentation = factory.roundWithBackground(tooltipPresentation)
144
+ return roundPresentation
145
+ }
146
+
147
+ fun onValidValue (setter : (value: String , project: Project ? ) -> Unit , project : Project ? ): (value: String ) -> Unit {
148
+ return { value ->
149
+ runAsync {
150
+ WriteCommandAction .runWriteCommandAction(project) {
151
+ setter.invoke(value, project)
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ private class Base64ValueAdapter (private val element : PsiElement ) {
159
+
160
+ private companion object {
161
+ private val CONTENT_REGEX = Regex (" [^ |\n ]*" , setOf (RegexOption .MULTILINE ))
162
+ private const val START_MULTILINE = " |\n "
163
+ }
164
+
165
+ fun set (value : String , project : Project ? ) {
166
+ val toSet = if (isMultiline()) {
167
+ START_MULTILINE + encodeBase64(value)
168
+ ?.chunked(76 )
169
+ ?.joinToString(" \n " )
170
+ } else {
171
+ encodeBase64(value)
172
+ }
173
+ ? : return
174
+ setValue(toSet, element, project)
175
+ }
176
+
177
+ fun get (): String? {
178
+ return getValue(element)
179
+ }
180
+
181
+ fun isMultiline (): Boolean {
182
+ val value = get()
183
+ return value?.startsWith(START_MULTILINE ) ? : false
184
+ }
185
+
186
+ fun getDecoded (): String? {
187
+ val value = get() ? : return null
188
+ val content = CONTENT_REGEX
189
+ .findAll(value)
190
+ .filter { matchResult -> matchResult.value.isNotBlank() }
191
+ .map { matchResult -> matchResult.value }
192
+ .joinToString(separator = " " )
193
+ return decodeBase64(content)
194
+ }
195
+
196
+ fun getStartOffset (): Int? {
197
+ return getStartOffset(element)
198
+ }
199
+ }
200
+ }
0 commit comments