1
+ package org.utbot.intellij.plugin.javadoc
2
+
3
+ import com.intellij.codeInsight.documentation.DocumentationManagerUtil
4
+ import com.intellij.codeInsight.javadoc.JavaDocUtil
5
+ import com.intellij.lang.documentation.DocumentationMarkup
6
+ import com.intellij.openapi.project.DumbService
7
+ import com.intellij.openapi.project.IndexNotReadyException
8
+ import com.intellij.openapi.util.text.StringUtil
9
+ import com.intellij.psi.*
10
+ import com.intellij.psi.javadoc.PsiDocComment
11
+ import com.intellij.psi.javadoc.PsiDocTag
12
+ import com.intellij.psi.javadoc.PsiDocToken
13
+ import com.intellij.psi.javadoc.PsiInlineDocTag
14
+ import mu.KotlinLogging
15
+
16
+ private const val LINK_TAG = " link"
17
+ private const val LINKPLAIN_TAG = " linkplain"
18
+ private const val LITERAL_TAG = " literal"
19
+ private const val CODE_TAG = " code"
20
+ private const val SYSTEM_PROPERTY_TAG = " systemProperty"
21
+ private const val MESSAGE_SEPARATOR = " :"
22
+ private const val PARAGRAPH_TAG = " <p>"
23
+ private const val CODE_TAG_START = " <code>"
24
+ private const val CODE_TAG_END = " </code>"
25
+
26
+ private val logger = KotlinLogging .logger {}
27
+
28
+ /* *
29
+ * Generates UtBot specific sections to include them to rendered JavaDoc comment.
30
+ *
31
+ * Methods responsible for value generation were taken from IJ platform class (they are private and couldn't be used outside).
32
+ *
33
+ * See [com.intellij.codeInsight.javadoc.JavaDocInfoGenerator].
34
+ *
35
+ * It wouldn't be needed to generate rendered doc on our own after updating to the IJ platform 2022.2,
36
+ * so delete it after updating and use basic [com.intellij.codeInsight.javadoc.JavaDocInfoGenerator].
37
+ */
38
+ class UtJavaDocInfoGenerator {
39
+ fun addUtBotSpecificSectionsToJavaDoc (javadoc : String? , comment : PsiDocComment ): String {
40
+ val builder = if (javadoc == null ) {
41
+ StringBuilder ()
42
+ } else {
43
+ StringBuilder (javadoc)
44
+ }
45
+
46
+ val docTagProvider = UtCustomJavaDocTagProvider ()
47
+ docTagProvider.supportedTags.forEach {
48
+ generateUtTagSection(builder, comment, it)
49
+ }
50
+ return builder.toString()
51
+ }
52
+
53
+ /* *
54
+ * Searches for UtBot tag in the comment and generates a related section for it.
55
+ */
56
+ private fun generateUtTagSection (
57
+ builder : StringBuilder ,
58
+ comment : PsiDocComment ,
59
+ utTag : UtCustomJavaDocTagProvider .UtCustomTagInfo
60
+ ) {
61
+ val tag = comment.findTagByName(utTag.name) ? : return
62
+ startHeaderSection(builder, utTag.getMessage()).append(PARAGRAPH_TAG )
63
+ val sectionContent = buildString {
64
+ generateValue(this , tag.dataElements)
65
+ trim()
66
+ }
67
+
68
+ builder.append(sectionContent)
69
+ builder.append(DocumentationMarkup .SECTION_END )
70
+ }
71
+
72
+ private fun startHeaderSection (builder : StringBuilder , message : String ): StringBuilder =
73
+ builder.append(DocumentationMarkup .SECTION_HEADER_START )
74
+ .append(message)
75
+ .append(MESSAGE_SEPARATOR )
76
+ .append(DocumentationMarkup .SECTION_SEPARATOR )
77
+
78
+ /* *
79
+ * Generates info depending on tag's value type.
80
+ */
81
+ private fun generateValue (builder : StringBuilder , elements : Array <PsiElement >) {
82
+ if (elements.isEmpty()) {
83
+ return
84
+ }
85
+
86
+ var offset = elements[0 ].textOffset + elements[0 ].text.length
87
+
88
+ for (element in elements) {
89
+ with (element) {
90
+ if (textOffset > offset) {
91
+ builder.append(' ' )
92
+ }
93
+
94
+ offset = textOffset + text.length
95
+
96
+ if (element is PsiInlineDocTag ) {
97
+ when (element.name) {
98
+ LITERAL_TAG -> generateLiteralValue(builder, element)
99
+ CODE_TAG , SYSTEM_PROPERTY_TAG -> generateCodeValue(element, builder)
100
+ LINK_TAG -> generateLinkValue(element, builder, false )
101
+ LINKPLAIN_TAG -> generateLinkValue(element, builder, true )
102
+ }
103
+ } else {
104
+ appendPlainText(builder, text)
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ private fun appendPlainText (builder : StringBuilder , text : String ) {
111
+ builder.append(StringUtil .replaceUnicodeEscapeSequences(text))
112
+ }
113
+
114
+ private fun collectElementText (builder : StringBuilder , element : PsiElement ) {
115
+ element.accept(object : PsiRecursiveElementWalkingVisitor () {
116
+ override fun visitElement (element : PsiElement ) {
117
+ super .visitElement(element)
118
+ if (element is PsiWhiteSpace ||
119
+ element is PsiJavaToken ||
120
+ element is PsiDocToken && element.tokenType != = JavaDocTokenType .DOC_COMMENT_LEADING_ASTERISKS
121
+ ) {
122
+ builder.append(element.text)
123
+ }
124
+ }
125
+ })
126
+ }
127
+
128
+ private fun generateCodeValue (tag : PsiInlineDocTag , builder : StringBuilder ) {
129
+ builder.append(CODE_TAG_START )
130
+ val pos = builder.length
131
+ generateLiteralValue(builder, tag)
132
+ builder.append(CODE_TAG_END )
133
+ if (builder[pos] == ' \n ' ) {
134
+ builder.insert(
135
+ pos,
136
+ ' '
137
+ ) // line break immediately after opening tag is ignored by JEditorPane
138
+ }
139
+ }
140
+
141
+ private fun generateLiteralValue (builder : StringBuilder , tag : PsiDocTag ) {
142
+ val literalValue = buildString {
143
+ val children = tag.children
144
+ for (i in 2 until children.size - 1 ) { // process all children except tag opening/closing elements
145
+ val child = children[i]
146
+ if (child is PsiDocToken && child.tokenType == = JavaDocTokenType .DOC_COMMENT_LEADING_ASTERISKS ) {
147
+ continue
148
+ }
149
+
150
+ var elementText = child.text
151
+ if (child is PsiWhiteSpace ) {
152
+ val pos = elementText.lastIndexOf(' \n ' )
153
+ if (pos >= 0 ) {
154
+ elementText = elementText.substring(0 , pos + 1 ) // skip whitespace before leading asterisk
155
+ }
156
+ }
157
+ appendPlainText(this , StringUtil .escapeXmlEntities(elementText))
158
+ }
159
+ }
160
+ builder.append(StringUtil .trimLeading(literalValue))
161
+ }
162
+
163
+ private fun generateLinkValue (tag : PsiInlineDocTag , builder : StringBuilder , plainLink : Boolean ) {
164
+ val tagElements = tag.dataElements
165
+ val linkText = createLinkText(tagElements)
166
+ if (linkText.isNotEmpty()) {
167
+ val index = JavaDocUtil .extractReference(linkText)
168
+ val referenceText = linkText.substring(0 , index).trim()
169
+ val label = StringUtil .nullize(linkText.substring(index).trim())
170
+ generateLink(builder, referenceText, label, tagElements[0 ], plainLink)
171
+ }
172
+ }
173
+
174
+ private fun createLinkText (tagElements : Array <PsiElement >): String {
175
+ var offset = if (tagElements.isNotEmpty()) {
176
+ tagElements[0 ].textOffset + tagElements[0 ].text.length
177
+ } else {
178
+ 0
179
+ }
180
+
181
+ return buildString {
182
+ for (i in tagElements.indices) {
183
+ val tagElement = tagElements[i]
184
+ if (tagElement.textOffset > offset) {
185
+ this .append(' ' )
186
+ }
187
+ offset = tagElement.textOffset + tagElement.text.length
188
+ collectElementText(this , tagElement)
189
+ if (i < tagElements.lastIndex) {
190
+ this .append(' ' )
191
+ }
192
+ }
193
+ }.trim()
194
+ }
195
+
196
+ private fun generateLink (
197
+ builder : StringBuilder ,
198
+ refText : String? ,
199
+ label : String? ,
200
+ context : PsiElement ,
201
+ plainLink : Boolean
202
+ ) {
203
+ val linkLabel = label ? : context.manager.let {
204
+ JavaDocUtil .getLabelText(it.project, it, refText, context)
205
+ }
206
+
207
+ var target: PsiElement ? = null
208
+ try {
209
+ if (refText != null ) {
210
+ target = JavaDocUtil .findReferenceTarget(context.manager, refText, context)
211
+ }
212
+ } catch (e: IndexNotReadyException ) {
213
+ logger.info(e) { " Failed to find a reference while generating JavaDoc comment. Details: ${e.message} " }
214
+ }
215
+
216
+ if (target == null && DumbService .isDumb(context.project)) {
217
+ builder.append(linkLabel)
218
+ } else if (target == null ) {
219
+ builder.append(" <font color=red>" ).append(linkLabel).append(" </font>" )
220
+ } else {
221
+ JavaDocUtil .getReferenceText(target.project, target)?.let {
222
+ DocumentationManagerUtil .createHyperlink(builder, target, it, linkLabel, plainLink)
223
+ }
224
+ }
225
+ }
226
+ }
0 commit comments