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