Skip to content

Commit a14d388

Browse files
committed
refactor: extract componentN filtering logic and add tests
1 parent 95cd50e commit a14d388

File tree

7 files changed

+168
-22
lines changed

7 files changed

+168
-22
lines changed
Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package codes.vg.betterkotlinjavacompletion
22

3+
import codes.vg.betterkotlinjavacompletion.dataclass.KotlinDataClassComponentNFilter
34
import com.intellij.codeInsight.completion.*
4-
import com.intellij.codeInsight.lookup.LookupElement
55
import com.intellij.lang.java.JavaLanguage
66
import com.intellij.patterns.PlatformPatterns
77
import com.intellij.psi.PsiJavaFile
88
import com.intellij.util.ProcessingContext
9-
import org.jetbrains.kotlin.asJava.elements.KtLightMethod
10-
import org.jetbrains.kotlin.psi.KtParameter
11-
import org.jetbrains.kotlin.psi.psiUtil.containingClass
129

13-
internal class KotlinDataClassCompletionContributor : CompletionContributor() {
10+
internal class KotlinCompletionContributor : CompletionContributor() {
11+
private val filters = setOf(
12+
KotlinDataClassComponentNFilter(),
13+
)
1414

1515
private val completionProvider = object : CompletionProvider<CompletionParameters>() {
1616
override fun addCompletions(
@@ -23,31 +23,19 @@ internal class KotlinDataClassCompletionContributor : CompletionContributor() {
2323
return
2424
result.runRemainingContributors(parameters) { completion ->
2525
val lookupElement = completion.lookupElement
26-
if (!shouldFilterOut(lookupElement)) {
26+
val shouldShow = filters.none { it.shouldFilterOut(lookupElement.psiElement, parameters) }
27+
if (shouldShow) {
2728
result.addElement(lookupElement)
2829
}
2930
}
3031
}
3132
}
3233

33-
private val componentNRegex = Regex("^component[1-9]\\d*$")
34-
3534
init {
3635
extend(
3736
CompletionType.BASIC,
3837
PlatformPatterns.psiElement().withLanguage(JavaLanguage.INSTANCE),
3938
completionProvider,
4039
)
4140
}
42-
43-
private fun shouldFilterOut(lookupElement: LookupElement): Boolean {
44-
val psiElement = lookupElement.psiElement as? KtLightMethod
45-
?: return false
46-
val ktOrigin = psiElement.kotlinOrigin
47-
?: return false
48-
val isParamInDataClass = ktOrigin.containingClass()?.isData() == true && ktOrigin is KtParameter // Generated `componentN()` in data classes are treated by Kotlin compiler as value parameters, not functions
49-
if (!isParamInDataClass)
50-
return false
51-
return componentNRegex.matches(psiElement.name)
52-
}
53-
}
41+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package codes.vg.betterkotlinjavacompletion
2+
3+
import com.intellij.codeInsight.completion.CompletionParameters
4+
import com.intellij.psi.PsiElement
5+
6+
internal interface KotlinCompletionFilter {
7+
fun shouldFilterOut(psiElement: PsiElement?, parameters: CompletionParameters): Boolean
8+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package codes.vg.betterkotlinjavacompletion.dataclass
2+
3+
import codes.vg.betterkotlinjavacompletion.KotlinCompletionFilter
4+
import com.intellij.codeInsight.completion.CompletionParameters
5+
import com.intellij.psi.PsiElement
6+
import org.jetbrains.kotlin.asJava.elements.KtLightMethod
7+
import org.jetbrains.kotlin.psi.KtParameter
8+
import org.jetbrains.kotlin.psi.psiUtil.containingClass
9+
10+
internal class KotlinDataClassComponentNFilter: KotlinCompletionFilter {
11+
12+
private val componentNRegex = Regex("^component[1-9]\\d*$")
13+
14+
override fun shouldFilterOut(psiElement: PsiElement?, parameters: CompletionParameters): Boolean {
15+
if (psiElement !is KtLightMethod)
16+
return false
17+
val ktOrigin = psiElement.kotlinOrigin
18+
?: return false
19+
val isParamInDataClass = ktOrigin.containingClass()?.isData() == true && ktOrigin is KtParameter // Generated `componentN()` in data classes are treated by Kotlin compiler as value parameters, not functions
20+
return isParamInDataClass && componentNRegex.matches(psiElement.name)
21+
}
22+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@
4646
<extensions defaultExtensionNs="com.intellij">
4747
<completion.contributor
4848
language="JAVA"
49-
implementationClass="codes.vg.betterkotlinjavacompletion.KotlinDataClassCompletionContributor"/>
49+
implementationClass="codes.vg.betterkotlinjavacompletion.KotlinCompletionContributor"/>
5050
</extensions>
51-
</idea-plugin>
51+
</idea-plugin>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package codes.vg.betterkotlinjavacompletion.dataclass
2+
3+
import codes.vg.betterkotlinjavacompletion.util.FilterCase
4+
import codes.vg.betterkotlinjavacompletion.util.completionParametersAtCaret
5+
import codes.vg.betterkotlinjavacompletion.util.lightClassMethod
6+
import com.intellij.ide.highlighter.JavaFileType
7+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
8+
9+
class KotlinDataClassComponentNFilterTest : BasePlatformTestCase() {
10+
11+
private lateinit var filter: KotlinDataClassComponentNFilter
12+
13+
override fun setUp() {
14+
super.setUp()
15+
filter = KotlinDataClassComponentNFilter()
16+
}
17+
18+
fun testComponentNFiltering() {
19+
myFixture.addFileToProject(
20+
"User.kt",
21+
"""
22+
package codes.vg.betterkotlinjavacompletion.dataclass
23+
24+
data class User(
25+
val firstName: String,
26+
val lastName: String,
27+
) {
28+
operator fun component3() = "${"$"}firstName ${"$"}lastName"
29+
operator fun component4() = firstName
30+
}
31+
""".trimIndent()
32+
)
33+
34+
myFixture.addFileToProject(
35+
"NotADataClass.kt",
36+
"""
37+
package codes.vg.betterkotlinjavacompletion.dataclass
38+
39+
class NotADataClass {
40+
operator fun component1() = "not to be filtered out"
41+
}
42+
""".trimIndent()
43+
)
44+
45+
val javaTemplate = """
46+
package codes.vg.betterkotlinjavacompletion.dataclass;
47+
48+
class DataClassCompletionTest {
49+
void test(%s instance) {
50+
instance.%s<caret>IntellijIdeaRulezzz
51+
}
52+
}
53+
""".trimIndent()
54+
55+
val cases = listOf(
56+
FilterCase("component1", "User", true), // generated by compiler
57+
FilterCase("component2", "User", true), // generated by compiler
58+
FilterCase("component3", "User", false), // declared by user
59+
FilterCase("component4", "User", false), // declared by user
60+
FilterCase("component1", "NotADataClass", false), // declared in an ordinary class
61+
)
62+
63+
cases.forEachIndexed { index, (methodName, className, expectedFiltered) ->
64+
val javaCode = javaTemplate.format(className, methodName)
65+
myFixture.configureByText(JavaFileType.INSTANCE, javaCode)
66+
val parameters = myFixture.completionParametersAtCaret()
67+
val method = myFixture.lightClassMethod(className, methodName)
68+
69+
val isFiltered = filter.shouldFilterOut(method, parameters)
70+
71+
assertEquals(
72+
"[$index] Unexpected filtering for $className.$methodName",
73+
expectedFiltered,
74+
isFiltered,
75+
)
76+
}
77+
}
78+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package codes.vg.betterkotlinjavacompletion.util
2+
3+
import com.intellij.codeInsight.completion.CompletionParameters
4+
import com.intellij.codeInsight.completion.CompletionProcess
5+
import com.intellij.codeInsight.completion.CompletionType
6+
import com.intellij.psi.PsiManager
7+
import com.intellij.psi.PsiMethod
8+
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
9+
import org.jetbrains.kotlin.asJava.toLightClass
10+
import org.jetbrains.kotlin.psi.KtClass
11+
import org.jetbrains.kotlin.psi.KtFile
12+
13+
internal fun CodeInsightTestFixture.completionParametersAtCaret(): CompletionParameters {
14+
val file = this.file
15+
val editor = this.editor
16+
val offset = editor.caretModel.offset
17+
return CompletionParameters(
18+
file.findElementAt(offset - 1)!!,
19+
file,
20+
CompletionType.BASIC,
21+
offset,
22+
1,
23+
editor,
24+
object : CompletionProcess {
25+
override fun isAutopopupCompletion() = false
26+
},
27+
)
28+
}
29+
30+
internal fun CodeInsightTestFixture.lightClassMethod(className: String, methodName: String): PsiMethod {
31+
val vFile = findFileInTempDir("$className.kt")
32+
?: error("$className.kt not found in temp dir")
33+
val psiFile = PsiManager.getInstance(project).findFile(vFile)
34+
?: error("PsiFile for $className.kt not found")
35+
val ktFile = psiFile as? KtFile
36+
?: error("PsiFile is not a KtFile")
37+
38+
val desiredClass = ktFile.declarations
39+
.filterIsInstance<KtClass>()
40+
.firstOrNull { it.name == className }
41+
?: error("$className class not found in KtFile")
42+
43+
val lightClass = desiredClass.toLightClass()
44+
?: error("No light class for $className")
45+
46+
return lightClass.methods.first { it.name == methodName }
47+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package codes.vg.betterkotlinjavacompletion.util
2+
3+
internal data class FilterCase(val methodName: String, val className: String, val shouldFilter: Boolean)

0 commit comments

Comments
 (0)