Skip to content

Commit 9b321c1

Browse files
authored
Merge pull request #226 from domaframework/fix/code-completion-suggestions-after-static-property-accesses
Fix code completion for static field access
2 parents f2067ee + 3bcb4b5 commit 9b321c1

File tree

13 files changed

+309
-63
lines changed

13 files changed

+309
-63
lines changed

src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiPatternUtil.kt

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ import com.intellij.patterns.PatternCondition
1919
import com.intellij.patterns.PlatformPatterns
2020
import com.intellij.patterns.PsiElementPattern
2121
import com.intellij.psi.PsiElement
22+
import com.intellij.psi.PsiWhiteSpace
2223
import com.intellij.psi.TokenType
24+
import com.intellij.psi.tree.IElementType
2325
import com.intellij.psi.util.PsiTreeUtil
2426
import com.intellij.psi.util.elementType
2527
import com.intellij.psi.util.prevLeaf
2628
import com.intellij.psi.util.prevLeafs
2729
import com.intellij.util.ProcessingContext
30+
import org.domaframework.doma.intellij.common.sql.directive.DirectiveCompletion
31+
import org.domaframework.doma.intellij.psi.SqlCustomElCommentExpr
2832
import org.domaframework.doma.intellij.psi.SqlElClass
2933
import org.domaframework.doma.intellij.psi.SqlElIdExpr
3034
import org.domaframework.doma.intellij.psi.SqlTypes
@@ -43,7 +47,28 @@ object PsiPatternUtil {
4347
override fun accepts(
4448
element: PsiElement,
4549
context: ProcessingContext?,
46-
): Boolean = PsiTreeUtil.getParentOfType(element, parentClass, true) != null
50+
): Boolean {
51+
val inComment = PsiTreeUtil.getParentOfType(element, parentClass, true) != null
52+
if (inComment) return true
53+
54+
// Even incomplete elements during input are analyzed as strings and included in the code completion process within block comments.
55+
var prevElement = PsiTreeUtil.prevLeaf(element, true)
56+
while (prevElement != null &&
57+
!(
58+
prevElement.nextSibling is SqlCustomElCommentExpr &&
59+
!prevElement.nextSibling.text.endsWith("*/")
60+
)
61+
) {
62+
prevElement = PsiTreeUtil.prevLeaf(prevElement, true)
63+
}
64+
65+
var endBlock = PsiTreeUtil.nextLeaf(element, true)
66+
while (endBlock != null && endBlock.elementType != SqlTypes.BLOCK_COMMENT_END) {
67+
endBlock = PsiTreeUtil.nextLeaf(endBlock, true)
68+
}
69+
70+
return prevElement != null && endBlock != null
71+
}
4772
},
4873
)
4974

@@ -55,7 +80,7 @@ object PsiPatternUtil {
5580
context: ProcessingContext?,
5681
): Boolean {
5782
val bindText = element.prevLeaf()?.text ?: ""
58-
val directiveSymbol = listOf("%", "@", "^", "#")
83+
val directiveSymbol = DirectiveCompletion.directiveSymbols
5984
return directiveSymbol.any {
6085
bindText.startsWith(it) ||
6186
(element.elementType == SqlTypes.EL_IDENTIFIER && element.prevLeaf()?.text == it) ||
@@ -104,7 +129,7 @@ object PsiPatternUtil {
104129

105130
/**
106131
* Get the string to search from the cursor position to the start of a block comment or a blank space
107-
* @return search Keyword
132+
* @return The string up to the specified character
108133
*/
109134
fun getBindSearchWord(
110135
originalFile: PsiElement,
@@ -126,4 +151,21 @@ object PsiPatternUtil {
126151
.substringAfter(symbol)
127152
return prefix
128153
}
154+
155+
fun getBindSearchWord(
156+
element: PsiElement,
157+
targetType: IElementType?,
158+
): MutableList<PsiElement> {
159+
var prevElement = PsiTreeUtil.prevLeaf(element, true)
160+
var prevElements = mutableListOf<PsiElement>()
161+
while (prevElement != null &&
162+
prevElement !is PsiWhiteSpace &&
163+
prevElement.elementType != targetType &&
164+
prevElement.elementType != SqlTypes.BLOCK_COMMENT_START
165+
) {
166+
prevElements.add(prevElement)
167+
prevElement = PsiTreeUtil.prevLeaf(prevElement, true)
168+
}
169+
return prevElements
170+
}
129171
}

src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiStaticElement.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.intellij.psi.JavaPsiFacade
1919
import com.intellij.psi.PsiClass
2020
import com.intellij.psi.PsiFile
2121
import com.intellij.psi.search.GlobalSearchScope
22+
import org.domaframework.doma.intellij.common.util.StringUtil
2223
import org.domaframework.doma.intellij.extension.getJavaClazz
2324
import org.domaframework.doma.intellij.psi.SqlElExpr
2425

@@ -32,10 +33,7 @@ class PsiStaticElement(
3233
private var fqdn = elExprList?.joinToString(".") { e -> e.text } ?: ""
3334

3435
constructor(elExprNames: String, file: PsiFile) : this(null, file) {
35-
fqdn =
36-
elExprNames
37-
.substringAfter("@")
38-
.substringBefore("@")
36+
fqdn = StringUtil.getSqlElClassText(elExprNames)
3937
}
4038

4139
fun getRefClazz(): PsiClass? {

src/main/kotlin/org/domaframework/doma/intellij/common/sql/CleanElementText.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
*/
1616
package org.domaframework.doma.intellij.common.sql
1717

18+
import org.domaframework.doma.intellij.common.util.StringUtil
19+
1820
/**
1921
* Exclude extra strings and block symbols added by IntelliJ operations a
2022
* nd format them into necessary elements
2123
*/
2224
fun cleanString(str: String): String {
2325
val intelliKIdeaRuleZzz = "IntellijIdeaRulezzz"
24-
return str
25-
.substringAfter("/*")
26-
.substringBefore("*/")
26+
return StringUtil
27+
.replaceBlockCommentStartEnd(str)
2728
.replace(intelliKIdeaRuleZzz, "")
2829
// TODO: Temporary support when using operators.
2930
// Remove the "== a" element because it is attached to the end.

src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/DirectiveCompletion.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ class DirectiveCompletion(
2626
private val caretNextText: String,
2727
private val result: CompletionResultSet,
2828
) {
29+
companion object {
30+
val directiveSymbols =
31+
listOf(
32+
"%",
33+
"#",
34+
"^",
35+
"@",
36+
)
37+
}
38+
2939
fun directiveHandle(symbol: String): Boolean {
3040
return when (symbol) {
3141
"%" ->

src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/StaticDirectiveHandler.kt

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package org.domaframework.doma.intellij.common.sql.directive
1818
import com.intellij.codeInsight.completion.CompletionResultSet
1919
import com.intellij.openapi.module.Module
2020
import com.intellij.psi.PsiElement
21+
import com.intellij.psi.PsiWhiteSpace
2122
import com.intellij.psi.util.PsiTreeUtil
2223
import com.intellij.psi.util.elementType
24+
import org.domaframework.doma.intellij.common.psi.PsiPatternUtil
2325
import org.domaframework.doma.intellij.common.sql.directive.collector.FunctionCallCollector
2426
import org.domaframework.doma.intellij.common.sql.directive.collector.StaticClassPackageCollector
2527
import org.domaframework.doma.intellij.common.sql.directive.collector.StaticPropertyCollector
@@ -38,14 +40,12 @@ class StaticDirectiveHandler(
3840
override fun directiveHandle(): Boolean {
3941
var handleResult = false
4042

41-
if (isNextStaticFieldAccess(element)) {
43+
if (isStaticFieldAccessTopElement(element)) {
4244
handleResult = staticDirectiveHandler(element, result)
4345
}
4446
if (handleResult) return true
4547

46-
if (PsiTreeUtil.nextLeaf(element)?.elementType == SqlTypes.AT_SIGN ||
47-
element.elementType == SqlTypes.AT_SIGN
48-
) {
48+
if (isSqlElClassCompletion()) {
4949
val module = element.module ?: return false
5050
handleResult =
5151
collectionModulePackages(
@@ -55,32 +55,95 @@ class StaticDirectiveHandler(
5555
}
5656
if (handleResult) return true
5757

58-
if (PsiTreeUtil.prevLeaf(element)?.elementType == SqlTypes.AT_SIGN) {
58+
if (PsiTreeUtil.prevLeaf(element, true)?.elementType == SqlTypes.AT_SIGN) {
5959
// Built-in function completion
6060
handleResult = builtInDirectiveHandler(element, result)
6161
}
6262
return handleResult
6363
}
6464

65-
private fun isNextStaticFieldAccess(element: PsiElement): Boolean {
66-
val prev = PsiTreeUtil.prevLeaf(element)
67-
return element.prevSibling is SqlElStaticFieldAccessExpr ||
65+
/**
66+
* Determines whether code completion is needed for [SqlElClass] elements.
67+
*/
68+
private fun isSqlElClassCompletion(): Boolean {
69+
if (element.elementType == SqlTypes.AT_SIGN &&
70+
PsiTreeUtil.prevLeaf(element)?.elementType == SqlTypes.AT_SIGN
71+
) {
72+
return true
73+
}
74+
75+
val elClassPattern = "^([a-zA-Z]*(\\.)+)*$"
76+
val regex = Regex(elClassPattern)
77+
val prevElements = PsiPatternUtil.getBindSearchWord(element, SqlTypes.AT_SIGN)
78+
val topAtSign = PsiTreeUtil.prevLeaf(prevElements.lastOrNull() ?: element, true)
79+
val prevWords = prevElements.reversed().joinToString("") { it.text }
80+
81+
// If the cursor is in the middle of [SqlElClass],
82+
// search for the following @ and ensure that code completion is within [SqlElClass].
83+
if (element.elementType != SqlTypes.AT_SIGN) {
84+
var nextElement = PsiTreeUtil.nextLeaf(element, true)
85+
while (nextElement != null &&
86+
nextElement !is PsiWhiteSpace &&
87+
nextElement.elementType != SqlTypes.BLOCK_COMMENT_END &&
88+
nextElement.elementType != SqlTypes.AT_SIGN
89+
) {
90+
nextElement = PsiTreeUtil.nextLeaf(nextElement, true)
91+
}
92+
val lastAtSign = PsiTreeUtil.nextLeaf(nextElement ?: element, true)
93+
if (regex.matches(prevWords) &&
94+
(
95+
lastAtSign == null ||
96+
nextElement.elementType != SqlTypes.AT_SIGN ||
97+
lastAtSign.elementType == SqlTypes.BLOCK_COMMENT_END
98+
)
99+
) {
100+
return false
101+
}
102+
}
103+
104+
// Check if there is a partially entered class package name ahead and ensure that input is in [SqlElClass].
105+
if (prevElements.isEmpty()) return false
106+
return (
107+
topAtSign?.elementType == SqlTypes.AT_SIGN &&
108+
PsiTreeUtil.prevLeaf(topAtSign, true)?.elementType != SqlTypes.EL_IDENTIFIER &&
109+
regex.matches(prevWords)
110+
)
111+
}
112+
113+
/**
114+
* Code completion for static properties after [SqlElClass].
115+
*/
116+
private fun isStaticFieldAccessTopElement(element: PsiElement): Boolean {
117+
val prev = PsiTreeUtil.prevLeaf(element, true)
118+
val staticFieldAccess =
119+
PsiTreeUtil.getParentOfType(prev, SqlElStaticFieldAccessExpr::class.java)
120+
val sqlElClassWords = PsiPatternUtil.getBindSearchWord(element.containingFile, element, " ")
121+
return (
122+
staticFieldAccess != null && staticFieldAccess.elIdExprList.isEmpty()
123+
) ||
68124
(
69125
prev?.elementType == SqlTypes.AT_SIGN &&
70126
prev.parent is SqlElStaticFieldAccessExpr
127+
) ||
128+
(
129+
sqlElClassWords.startsWith("@") &&
130+
sqlElClassWords.endsWith("@")
71131
)
72132
}
73133

74134
private fun staticDirectiveHandler(
75135
element: PsiElement,
76136
result: CompletionResultSet,
77137
): Boolean {
138+
val prev = PsiTreeUtil.prevLeaf(element, true)
78139
val clazzRef =
79140
PsiTreeUtil
80-
.getChildOfType(element.prevSibling, SqlElClass::class.java)
141+
.getChildOfType(prev, SqlElClass::class.java)
81142
?: PsiTreeUtil.getChildOfType(PsiTreeUtil.prevLeaf(element)?.parent, SqlElClass::class.java)
82-
val fqdn =
83-
PsiTreeUtil.getChildrenOfTypeAsList(clazzRef, PsiElement::class.java).joinToString("") { it.text }
143+
144+
val sqlElClassWords = PsiPatternUtil.getBindSearchWord(element.containingFile, element, " ")
145+
val sqlElClassName = PsiTreeUtil.getChildrenOfTypeAsList(clazzRef, PsiElement::class.java).joinToString("") { it.text }
146+
val fqdn = if (sqlElClassName.isNotEmpty()) sqlElClassName else sqlElClassWords.replace("@", "")
84147

85148
val collector = StaticPropertyCollector(element, caretNextText, bindText)
86149
val candidates = collector.collectCompletionSuggest(fqdn) ?: return false

src/main/kotlin/org/domaframework/doma/intellij/common/util/ForDirectiveUtil.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class ForDirectiveUtil {
6767
private val cachedForDirectiveBlocks: MutableMap<PsiElement, CachedValue<List<BlockToken>>> =
6868
mutableMapOf()
6969

70+
const val HAS_NEXT_PREFIX = "_has_next"
71+
const val INDEX_PREFIX = "_index"
72+
7073
/**
7174
* Get the parent for directive list to which this directive belongs
7275
* @param targetElement Element to search for definer
@@ -162,7 +165,7 @@ class ForDirectiveUtil {
162165
skipSelf: Boolean = true,
163166
forDirectives: List<BlockToken> = getForDirectiveBlocks(targetElement, skipSelf),
164167
): PsiElement? {
165-
val searchText = targetElement.text.replace("_has_next", "").replace("_index", "")
168+
val searchText = targetElement.text.replace(HAS_NEXT_PREFIX, "").replace(INDEX_PREFIX, "")
166169
return forDirectives.firstOrNull { it.item.text == searchText }?.item
167170
}
168171

@@ -507,9 +510,9 @@ class ForDirectiveUtil {
507510
}
508511

509512
fun resolveForDirectiveItemClassTypeBySuffixElement(searchName: String): PsiType? =
510-
if (searchName.endsWith("_has_next")) {
513+
if (searchName.endsWith(HAS_NEXT_PREFIX)) {
511514
PsiTypes.booleanType()
512-
} else if (searchName.endsWith("_index")) {
515+
} else if (searchName.endsWith(INDEX_PREFIX)) {
513516
PsiTypes.intType()
514517
} else {
515518
null
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.common.util
17+
18+
object StringUtil {
19+
fun getSqlElClassText(text: String): String =
20+
text
21+
.substringAfter("@")
22+
.substringBefore("@")
23+
24+
fun replaceBlockCommentStartEnd(text: String): String = text.substringAfter("/*").substringBefore("*/")
25+
}

0 commit comments

Comments
 (0)