Skip to content

Commit b1cf7df

Browse files
committed
Add ForItemElementDocumentationProvider for enhanced documentation support
1 parent 434a07a commit b1cf7df

File tree

3 files changed

+254
-18
lines changed

3 files changed

+254
-18
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright Doma Tools Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.domaframework.doma.intellij.document
17+
18+
import com.intellij.lang.documentation.AbstractDocumentationProvider
19+
import com.intellij.psi.PsiClassType
20+
import com.intellij.psi.PsiElement
21+
import org.domaframework.doma.intellij.common.dao.findDaoMethod
22+
import org.domaframework.doma.intellij.common.psi.PsiParentClass
23+
import org.domaframework.doma.intellij.common.sql.PsiClassTypeUtil
24+
import org.domaframework.doma.intellij.common.sql.foritem.ForItem
25+
import org.domaframework.doma.intellij.common.sql.validator.SqlElForItemFieldAccessorChildElementValidator
26+
import org.domaframework.doma.intellij.common.sql.validator.result.ValidationCompleteResult
27+
import org.domaframework.doma.intellij.common.sql.validator.result.ValidationResult
28+
import org.domaframework.doma.intellij.extension.psi.getForItem
29+
import org.domaframework.doma.intellij.extension.psi.getForItemDeclaration
30+
import org.domaframework.doma.intellij.inspection.ForDirectiveInspection
31+
import org.domaframework.doma.intellij.psi.SqlElForDirective
32+
import org.domaframework.doma.intellij.psi.SqlElIdExpr
33+
import org.domaframework.doma.intellij.psi.SqlTypes
34+
import org.toml.lang.psi.ext.elementType
35+
import java.util.LinkedList
36+
37+
class ForItemElementDocumentationProvider : AbstractDocumentationProvider() {
38+
override fun generateDoc(
39+
element: PsiElement?,
40+
originalElement: PsiElement?,
41+
): String? {
42+
val result: MutableList<String?> = LinkedList<String?>()
43+
if (originalElement !is SqlElIdExpr && originalElement?.elementType != SqlTypes.EL_IDENTIFIER) {
44+
return super.generateDoc(
45+
element,
46+
originalElement,
47+
)
48+
}
49+
50+
val file = originalElement.containingFile
51+
val daoMethod = findDaoMethod(file) ?: return ""
52+
val forDirectiveInspection = ForDirectiveInspection(daoMethod, "")
53+
54+
val currentForItem = ForItem(originalElement)
55+
val forDirectiveExpr = currentForItem.getParentForDirectiveExpr()
56+
if (forDirectiveExpr != null) {
57+
if (forDirectiveExpr.getForItem()?.textOffset != originalElement.textOffset) {
58+
val declarationClassType =
59+
forDirectiveInspection.validateFieldAccessByForItem(
60+
listOf(originalElement),
61+
skipSelf = false,
62+
)
63+
if (declarationClassType != null) {
64+
generateDocumentInForDirective(
65+
declarationClassType,
66+
forDirectiveExpr,
67+
originalElement,
68+
result,
69+
)
70+
}
71+
} else {
72+
val declarationSide = forDirectiveExpr.getForItemDeclaration() ?: return ""
73+
val children = declarationSide.getDeclarationChildren()
74+
generateDocumentInForItem(
75+
forDirectiveInspection,
76+
originalElement,
77+
children,
78+
result,
79+
)
80+
}
81+
} else {
82+
generateDocumentInBindVariable(forDirectiveInspection, originalElement, result)
83+
}
84+
return result.joinToString("\n")
85+
}
86+
87+
private fun generateDocumentInForItem(
88+
forDirectiveInspection: ForDirectiveInspection,
89+
originalElement: PsiElement,
90+
declarationChildren: List<SqlElIdExpr>,
91+
result: MutableList<String?>,
92+
) {
93+
val declarationClassType =
94+
forDirectiveInspection.validateFieldAccessByForItem(listOf(originalElement), false)
95+
if (declarationClassType is ValidationCompleteResult) {
96+
val parentClass = declarationClassType.parentClass
97+
val forItemValidator =
98+
SqlElForItemFieldAccessorChildElementValidator(
99+
declarationChildren,
100+
parentClass,
101+
)
102+
val declarationClassTypeResult = forItemValidator.validateChildren()
103+
if (declarationClassTypeResult is ValidationCompleteResult) {
104+
var resultParent = declarationClassTypeResult.parentClass
105+
var classType: PsiClassType? = resultParent.type as? PsiClassType
106+
if (classType != null &&
107+
PsiClassTypeUtil.isIterableType(classType, originalElement.project)
108+
) {
109+
classType = classType.parameters.firstOrNull() as? PsiClassType
110+
}
111+
if (classType != null) {
112+
resultParent = PsiParentClass(classType)
113+
result.add("${generateTypeLink(resultParent)} ${originalElement.text}")
114+
}
115+
}
116+
}
117+
}
118+
119+
private fun generateDocumentInBindVariable(
120+
forDirectiveInspection: ForDirectiveInspection,
121+
originalElement: PsiElement,
122+
result: MutableList<String?>,
123+
) {
124+
val declarationClassType =
125+
forDirectiveInspection.validateFieldAccessByForItem(listOf(originalElement))
126+
if (declarationClassType != null) {
127+
val parentClass = declarationClassType.parentClass
128+
result.add("${generateTypeLink(parentClass)} ${originalElement.text}")
129+
}
130+
}
131+
132+
private fun generateDocumentInForDirective(
133+
declarationClassType: ValidationResult,
134+
forDirectiveExpr: SqlElForDirective,
135+
originalElement: PsiElement,
136+
result: MutableList<String?>,
137+
) {
138+
val parentClass = declarationClassType.parentClass
139+
val parentType = parentClass?.type as? PsiClassType
140+
141+
if (forDirectiveExpr.getForItem()?.textOffset == originalElement.textOffset) {
142+
generateDocumentForItemSelf(parentType, originalElement, result)
143+
} else {
144+
result.add("${generateTypeLink(parentClass)} ${originalElement.text}")
145+
}
146+
}
147+
148+
private fun generateDocumentForItemSelf(
149+
parentType: PsiClassType?,
150+
originalElement: PsiElement,
151+
result: MutableList<String?>,
152+
) {
153+
val targetClassType =
154+
if (parentType != null &&
155+
PsiClassTypeUtil.isIterableType(parentType, originalElement.project)
156+
) {
157+
parentType.parameters.firstOrNull()
158+
} else {
159+
null
160+
}
161+
162+
if (targetClassType != null) {
163+
val forItemParentClassType = PsiParentClass(targetClassType)
164+
result.add("${generateTypeLink(forItemParentClassType)} ${originalElement.text}")
165+
}
166+
}
167+
168+
override fun generateHoverDoc(
169+
element: PsiElement,
170+
originalElement: PsiElement?,
171+
): String? = generateDoc(element, originalElement)
172+
173+
override fun getQuickNavigateInfo(
174+
element: PsiElement,
175+
originalElement: PsiElement?,
176+
): String? {
177+
val result: MutableList<String?> = LinkedList<String?>()
178+
val typeDocument = generateDoc(element, originalElement)
179+
result.add(typeDocument)
180+
return result.joinToString("\n")
181+
}
182+
183+
private fun generateTypeLink(parentClass: PsiParentClass?): String {
184+
if (parentClass?.type != null) {
185+
return generateTypeLinkFromCanonicalText(parentClass.type.canonicalText)
186+
}
187+
return ""
188+
}
189+
190+
private fun generateTypeLinkFromCanonicalText(canonicalText: String): String {
191+
val regex = Regex("([a-zA-Z0-9_]+\\.)*([a-zA-Z0-9_]+)")
192+
val result = StringBuilder()
193+
var lastIndex = 0
194+
195+
for (match in regex.findAll(canonicalText)) {
196+
val fullMatch = match.value
197+
val typeName = match.groups[2]?.value ?: fullMatch
198+
val startIndex = match.range.first
199+
val endIndex = match.range.last + 1
200+
201+
if (lastIndex < startIndex) {
202+
result.append(canonicalText.substring(lastIndex, startIndex))
203+
}
204+
result.append("<a href=\"psi_element://$fullMatch\">$typeName</a>")
205+
lastIndex = endIndex
206+
}
207+
208+
if (lastIndex < canonicalText.length) {
209+
result.append(canonicalText.substring(lastIndex))
210+
}
211+
212+
return result.toString()
213+
}
214+
}

src/main/kotlin/org/domaframework/doma/intellij/inspection/ForDirectiveInspection.kt

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ class ForDirectiveInspection(
6262
private val cachedForDirectiveBlocks: MutableMap<PsiElement, CachedValue<List<BlockToken>>> =
6363
mutableMapOf()
6464

65-
fun getForItem(targetElement: PsiElement): ForItem? {
66-
val forBlocks = getForDirectiveBlock(targetElement)
65+
fun getForItem(
66+
targetElement: PsiElement,
67+
skipSelf: Boolean = true,
68+
): ForItem? {
69+
val forBlocks = getForDirectiveBlock(targetElement, skipSelf)
6770
return getForItem(targetElement, forBlocks)
6871
}
6972

@@ -83,11 +86,14 @@ class ForDirectiveInspection(
8386
* Analyze the field access of the for item definition and finally get the declared type
8487
* @return [ValidationResult] is used to display the analysis results.
8588
*/
86-
fun validateFieldAccessByForItem(blockElements: List<PsiElement>): ValidationResult? {
89+
fun validateFieldAccessByForItem(
90+
blockElements: List<PsiElement>,
91+
skipSelf: Boolean = true,
92+
): ValidationResult? {
8793
val targetElement: PsiElement = blockElements.firstOrNull() ?: return null
8894
val topElm = blockElements.first()
8995

90-
val forDirectives = getForDirectiveBlock(targetElement)
96+
val forDirectives = getForDirectiveBlock(targetElement, skipSelf)
9197
val forItem = getForItem(targetElement, forDirectives) ?: return createErrorResult(targetElement)
9298
val domaAnnotationType = daoMethod.getDomaAnnotationType()
9399

@@ -114,8 +120,8 @@ class ForDirectiveInspection(
114120
topElm: PsiElement,
115121
): PsiClassType? {
116122
var nestClassType: PsiClassType? = if (isBatchAnnotation) initialType.parameters.firstOrNull() as? PsiClassType else initialType
117-
var listIndex = 1
118123

124+
// Follow the For directive and get the definition type of the current directive from the top definition type.
119125
for ((i, targetForDirective) in forDirectives.withIndex()) {
120126
if (nestClassType == null) break
121127
val targetDirectiveParent =
@@ -128,26 +134,28 @@ class ForDirectiveInspection(
128134
topElm,
129135
SqlElForDirective::class.java,
130136
)
137+
138+
// Skip if the For directive in the search target is the same directory as the self.
131139
if (targetDirectiveParent == targetElementParent) continue
132140

133141
val currentForItem = ForItem(targetForDirective.item)
134142
val currentDeclaration = currentForItem.getParentForDirectiveExpr()?.getForItemDeclaration() ?: continue
135-
136143
val declarationType = processDeclarationElement(currentDeclaration, nestClassType, i)
137144
if (declarationType != null) {
138145
if (!PsiClassTypeUtil.isIterableType(declarationType, topElm.project)) {
139146
return null
140147
}
141148
nestClassType = declarationType.parameters.firstOrNull() as? PsiClassType
142-
listIndex = 1
143149
} else {
144-
nestClassType = processListType(nestClassType, listIndex, topElm)
145-
listIndex++
150+
nestClassType = processListType(nestClassType, topElm)
146151
}
147152
}
148153
return nestClassType
149154
}
150155

156+
/**
157+
* Check the For directive definition element of the search target and get the declaration type
158+
*/
151159
private fun processDeclarationElement(
152160
currentDeclaration: ForDeclarationItem,
153161
nestClassType: PsiClassType,
@@ -162,23 +170,25 @@ class ForDirectiveInspection(
162170

163171
val declarationChildren = currentDeclaration.getDeclarationChildren()
164172
if (declarationChildren.size > 1) {
173+
// Gets the type of reference property if it is defined with field access
165174
val validator = SqlElForItemFieldAccessorChildElementValidator(declarationChildren, PsiParentClass(nestClassType), shortName)
166175
return validator.validateChildren()?.parentClass?.type as? PsiClassType
167176
}
168177
return null
169178
}
170179

180+
/**
181+
* Dig down the List definition type for the number of nesting levels
182+
* from the top For directive definition type to obtain the type that applies to the current directive.
183+
*/
171184
private fun processListType(
172185
nestClassType: PsiClassType,
173-
listIndex: Int,
174186
topElm: PsiElement,
175187
): PsiClassType? {
176188
var currentType: PsiClassType? = nestClassType
177-
repeat(listIndex) {
178-
currentType?.let { type ->
179-
if (PsiClassTypeUtil.isIterableType(type, topElm.project)) {
180-
currentType = type.parameters.firstOrNull() as? PsiClassType
181-
}
189+
currentType?.let { type ->
190+
if (PsiClassTypeUtil.isIterableType(type, topElm.project)) {
191+
currentType = type.parameters.firstOrNull() as? PsiClassType
182192
}
183193
}
184194
return currentType
@@ -191,7 +201,10 @@ class ForDirectiveInspection(
191201
* to the target element (`targetElement`)
192202
* and obtain the `for` block information to which the `targetElement` belongs.
193203
*/
194-
private fun getForDirectiveBlock(targetElement: PsiElement): List<BlockToken> {
204+
private fun getForDirectiveBlock(
205+
targetElement: PsiElement,
206+
skipSelf: Boolean = true,
207+
): List<BlockToken> {
195208
val cachedValue =
196209
cachedForDirectiveBlocks.getOrPut(targetElement) {
197210
CachedValuesManager.getManager(targetElement.project).createCachedValue {
@@ -222,7 +235,12 @@ class ForDirectiveInspection(
222235
}
223236
}
224237
val stack = mutableListOf<BlockToken>()
225-
val filterPosition = directiveBlocks.filter { it.position < targetElement.textOffset }
238+
val filterPosition =
239+
if (skipSelf) {
240+
directiveBlocks.filter { it.position < targetElement.textOffset }
241+
} else {
242+
directiveBlocks.filter { it.position <= targetElement.textOffset }
243+
}
226244
filterPosition.forEach { block ->
227245
when (block.type) {
228246
BlockType.FOR, BlockType.IF -> stack.add(block)

src/main/resources/META-INF/plugin.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
instance="org.domaframework.doma.intellij.setting.DomaToolsConfigurable"
2525
displayName="Doma Tools" />
2626

27+
<!-- Reference -->
2728
<psi.referenceContributor
2829
implementation="org.domaframework.doma.intellij.reference.SqlReferenceContributor"
2930
language="DomaSql"
3031
/>
31-
3232
<lang.elementManipulator forClass="org.domaframework.doma.intellij.psi.SqlCustomElExpr"
3333
implementationClass="org.domaframework.doma.intellij.reference.SqlElExprManipulator" />
3434

35+
<!-- Document -->
36+
<lang.documentationProvider language="DomaSql"
37+
implementationClass="org.domaframework.doma.intellij.document.ForItemElementDocumentationProvider" />
38+
3539
<!-- Gutter -->
3640
<codeInsight.lineMarkerProvider language="JAVA"
3741
implementationClass="org.domaframework.doma.intellij.gutter.dao.DaoMethodProvider"/>

0 commit comments

Comments
 (0)