Skip to content

Commit e066a6c

Browse files
authored
Merge pull request #126 from domaframework/feature/symbol-document
Add For Item Element Documentation
2 parents ef9d0ef + 6ee9cc7 commit e066a6c

File tree

13 files changed

+508
-20
lines changed

13 files changed

+508
-20
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
val declarationClassType =
58+
forDirectiveInspection.validateFieldAccessByForItem(
59+
listOf(originalElement),
60+
skipSelf = false,
61+
)
62+
if (declarationClassType != null) {
63+
generateDocumentInForDirective(
64+
forDirectiveInspection,
65+
declarationClassType,
66+
forDirectiveExpr,
67+
originalElement,
68+
result,
69+
)
70+
}
71+
} else {
72+
generateDocumentInBindVariable(forDirectiveInspection, originalElement, result)
73+
}
74+
return result.joinToString("\n")
75+
}
76+
77+
private fun generateDocumentInForItem(
78+
originalElement: PsiElement,
79+
declarationChildren: List<SqlElIdExpr>,
80+
declarationClassType: PsiParentClass,
81+
result: MutableList<String?>,
82+
) {
83+
val parentClass = declarationClassType
84+
val forItemValidator =
85+
SqlElForItemFieldAccessorChildElementValidator(
86+
declarationChildren,
87+
parentClass,
88+
)
89+
forItemValidator.validateChildren(complete = { lastType ->
90+
var resultParent = lastType
91+
var classType: PsiClassType? = resultParent.type as? PsiClassType
92+
if (classType != null &&
93+
PsiClassTypeUtil.isIterableType(classType, originalElement.project)
94+
) {
95+
classType = classType.parameters.firstOrNull() as? PsiClassType
96+
}
97+
if (classType != null) {
98+
resultParent = PsiParentClass(classType)
99+
result.add("${generateTypeLink(resultParent)} ${originalElement.text}")
100+
}
101+
})
102+
}
103+
104+
private fun generateDocumentInBindVariable(
105+
forDirectiveInspection: ForDirectiveInspection,
106+
originalElement: PsiElement,
107+
result: MutableList<String?>,
108+
) {
109+
val declarationClassType =
110+
forDirectiveInspection.validateFieldAccessByForItem(listOf(originalElement))
111+
if (declarationClassType != null) {
112+
val parentClass = declarationClassType.parentClass
113+
result.add("${generateTypeLink(parentClass)} ${originalElement.text}")
114+
}
115+
}
116+
117+
private fun generateDocumentInForDirective(
118+
forDirectiveInspection: ForDirectiveInspection,
119+
declarationClassType: ValidationResult,
120+
forDirectiveExpr: SqlElForDirective,
121+
originalElement: PsiElement,
122+
result: MutableList<String?>,
123+
) {
124+
val parentClass = declarationClassType.parentClass
125+
val parentType = parentClass?.type as? PsiClassType
126+
127+
if (forDirectiveExpr.getForItem()?.textOffset != originalElement.textOffset) {
128+
generateDocumentForItemSelf(parentType, originalElement, result)
129+
} else {
130+
val declarationSide = forDirectiveExpr.getForItemDeclaration() ?: return
131+
val children = declarationSide.getDeclarationChildren()
132+
133+
val declarationClassType =
134+
forDirectiveInspection.validateFieldAccessByForItem(listOf(originalElement), false)
135+
if (declarationClassType is ValidationCompleteResult) {
136+
generateDocumentInForItem(
137+
originalElement,
138+
children,
139+
declarationClassType.parentClass,
140+
result,
141+
)
142+
}
143+
}
144+
}
145+
146+
private fun generateDocumentForItemSelf(
147+
parentType: PsiClassType?,
148+
originalElement: PsiElement,
149+
result: MutableList<String?>,
150+
) {
151+
if (parentType != null) {
152+
val forItemParentClassType = PsiParentClass(parentType)
153+
result.add("${generateTypeLink(forItemParentClassType)} ${originalElement.text}")
154+
}
155+
}
156+
157+
override fun generateHoverDoc(
158+
element: PsiElement,
159+
originalElement: PsiElement?,
160+
): String? = generateDoc(element, originalElement)
161+
162+
override fun getQuickNavigateInfo(
163+
element: PsiElement,
164+
originalElement: PsiElement?,
165+
): String? {
166+
val result: MutableList<String?> = LinkedList<String?>()
167+
val typeDocument = generateDoc(element, originalElement)
168+
result.add(typeDocument)
169+
return result.joinToString("\n")
170+
}
171+
172+
private fun generateTypeLink(parentClass: PsiParentClass?): String {
173+
if (parentClass?.type != null) {
174+
return generateTypeLinkFromCanonicalText(parentClass.type.canonicalText)
175+
}
176+
return ""
177+
}
178+
179+
private fun generateTypeLinkFromCanonicalText(canonicalText: String): String {
180+
val regex = Regex("([a-zA-Z0-9_]+\\.)*([a-zA-Z0-9_]+)")
181+
val result = StringBuilder()
182+
var lastIndex = 0
183+
184+
for (match in regex.findAll(canonicalText)) {
185+
val fullMatch = match.value
186+
val typeName = match.groups[2]?.value ?: fullMatch
187+
val startIndex = match.range.first
188+
val endIndex = match.range.last + 1
189+
190+
if (lastIndex < startIndex) {
191+
result.append(canonicalText.substring(lastIndex, startIndex))
192+
}
193+
result.append("<a href=\"psi_element://$fullMatch\">$typeName</a>")
194+
lastIndex = endIndex
195+
}
196+
197+
if (lastIndex < canonicalText.length) {
198+
result.append(canonicalText.substring(lastIndex))
199+
}
200+
201+
return result.toString()
202+
}
203+
}

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)