FIR IDE: add type arguments and -> symbol to when conditions completion

This commit is contained in:
Ilya Kirillov
2021-05-31 15:32:39 +02:00
committed by TeamCityServer
parent fa2f2c34cb
commit 2e57361457
8 changed files with 166 additions and 25 deletions
@@ -5,14 +5,21 @@
package org.jetbrains.kotlin.idea.completion package org.jetbrains.kotlin.idea.completion
import org.jetbrains.kotlin.idea.frontend.api.analyse import com.intellij.codeInsight.completion.CompletionInitializationContext
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassKind import com.intellij.codeInsight.completion.CompletionUtilCore
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassOrObjectSymbol import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionSymbol
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtWhenEntry
class FirCompletionDummyIdentifierProviderService : CompletionDummyIdentifierProviderService() { class FirCompletionDummyIdentifierProviderService : CompletionDummyIdentifierProviderService() {
override fun handleDefaultCase(context: CompletionInitializationContext): String? {
val elementAtOffset = context.file.findElementAt(context.startOffset) ?: return null
return when {
elementAtOffset.parentOfType<KtWhenEntry>() != null -> CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED
else -> null
}
}
override fun allTargetsAreFunctionsOrClasses(nameReferenceExpression: KtNameReferenceExpression): Boolean { override fun allTargetsAreFunctionsOrClasses(nameReferenceExpression: KtNameReferenceExpression): Boolean {
return true return true
// TODO fir cannot handle invalid code and handles listOf< as binary expression // TODO fir cannot handle invalid code and handles listOf< as binary expression
@@ -24,10 +24,15 @@ import org.jetbrains.kotlin.idea.frontend.api.symbols.*
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.idea.completion.contributors.helpers.FirClassifierProvider.getAvailableClassifiersCurrentScope import org.jetbrains.kotlin.idea.completion.contributors.helpers.FirClassifierProvider.getAvailableClassifiersCurrentScope
import org.jetbrains.kotlin.idea.completion.contributors.helpers.addSymbolToCompletion
import org.jetbrains.kotlin.idea.completion.contributors.helpers.addTypeArguments
import org.jetbrains.kotlin.idea.completion.contributors.helpers.createStarTypeArgumentsList
import org.jetbrains.kotlin.idea.completion.lookups.KotlinLookupObject import org.jetbrains.kotlin.idea.completion.lookups.KotlinLookupObject
import org.jetbrains.kotlin.idea.completion.lookups.shortenReferencesForFirCompletion import org.jetbrains.kotlin.idea.completion.lookups.shortenReferencesForFirCompletion
import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtNamedSymbol import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtNamedSymbol
import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtSymbolWithTypeParameters
import org.jetbrains.kotlin.idea.frontend.api.types.* import org.jetbrains.kotlin.idea.frontend.api.types.*
import org.jetbrains.kotlin.miniStdLib.letIf
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.Name
@@ -44,15 +49,22 @@ internal class FirWhenWithSubjectConditionContributor(
val subjectType = subject.getKtType() val subjectType = subject.getKtType()
val classSymbol = getClassSymbol(subjectType) val classSymbol = getClassSymbol(subjectType)
val visibilityChecker = CompletionVisibilityChecker.create(basicContext, positionContext) val visibilityChecker = CompletionVisibilityChecker.create(basicContext, positionContext)
val isSingleCondition = whenCondition.isSingleConditionInEntry()
when { when {
classSymbol?.classKind == KtClassKind.ENUM_CLASS -> { classSymbol?.classKind == KtClassKind.ENUM_CLASS -> {
completeEnumEntries(classSymbol, allConditionsExceptCurrent, visibilityChecker) completeEnumEntries(classSymbol, allConditionsExceptCurrent, visibilityChecker, isSingleCondition)
} }
classSymbol?.modality == Modality.SEALED -> { classSymbol?.modality == Modality.SEALED -> {
completeSubClassesOfSealedClass(classSymbol, allConditionsExceptCurrent, whenCondition, visibilityChecker) completeSubClassesOfSealedClass(
classSymbol,
allConditionsExceptCurrent,
whenCondition,
visibilityChecker,
isSingleCondition
)
} }
else -> { else -> {
completeAllTypes(whenCondition, visibilityChecker) completeAllTypes(whenCondition, visibilityChecker, isSingleCondition)
} }
} }
addNullIfWhenExpressionCanReturnNull(subjectType) addNullIfWhenExpressionCanReturnNull(subjectType)
@@ -75,6 +87,7 @@ internal class FirWhenWithSubjectConditionContributor(
private fun KtAnalysisSession.completeAllTypes( private fun KtAnalysisSession.completeAllTypes(
whenCondition: KtWhenCondition, whenCondition: KtWhenCondition,
visibilityChecker: CompletionVisibilityChecker, visibilityChecker: CompletionVisibilityChecker,
isSingleCondition: Boolean,
) { ) {
getAvailableClassifiersCurrentScope(originalKtFile, whenCondition, scopeNameFilter, indexHelper, visibilityChecker) getAvailableClassifiersCurrentScope(originalKtFile, whenCondition, scopeNameFilter, indexHelper, visibilityChecker)
.forEach { classifier -> .forEach { classifier ->
@@ -84,7 +97,8 @@ internal class FirWhenWithSubjectConditionContributor(
classifier.name.asString(), classifier.name.asString(),
classifier, classifier,
(classifier as? KtNamedClassOrObjectSymbol)?.classIdIfNonLocal?.asSingleFqName(), (classifier as? KtNamedClassOrObjectSymbol)?.classIdIfNonLocal?.asSingleFqName(),
isPrefixNeeded(classifier) isPrefixNeeded(classifier),
isSingleCondition,
) )
} }
} }
@@ -104,6 +118,7 @@ internal class FirWhenWithSubjectConditionContributor(
conditions: List<KtWhenCondition>, conditions: List<KtWhenCondition>,
whenCondition: KtWhenCondition, whenCondition: KtWhenCondition,
visibilityChecker: CompletionVisibilityChecker, visibilityChecker: CompletionVisibilityChecker,
isSingleCondition: Boolean,
) { ) {
require(classSymbol.modality == Modality.SEALED) require(classSymbol.modality == Modality.SEALED)
val handledCasesClassIds = getHandledClassIds(conditions) val handledCasesClassIds = getHandledClassIds(conditions)
@@ -115,11 +130,17 @@ internal class FirWhenWithSubjectConditionContributor(
.filter { with(visibilityChecker) { isVisible(it as KtClassifierSymbol) } } .filter { with(visibilityChecker) { isVisible(it as KtClassifierSymbol) } }
.forEach { inheritor -> .forEach { inheritor ->
val classId = inheritor.classIdIfNonLocal ?: return@forEach val classId = inheritor.classIdIfNonLocal ?: return@forEach
addLookupElement(classId.relativeClassName.asString(), inheritor, classId.asSingleFqName(), isPrefixNeeded(inheritor)) addLookupElement(
classId.relativeClassName.asString(),
inheritor,
classId.asSingleFqName(),
isPrefixNeeded(inheritor),
isSingleCondition
)
} }
if (allInheritors.any { it.modality == Modality.ABSTRACT }) { if (allInheritors.any { it.modality == Modality.ABSTRACT }) {
completeAllTypes(whenCondition, visibilityChecker) completeAllTypes(whenCondition, visibilityChecker, isSingleCondition)
} }
} }
@@ -162,7 +183,8 @@ internal class FirWhenWithSubjectConditionContributor(
private fun KtAnalysisSession.completeEnumEntries( private fun KtAnalysisSession.completeEnumEntries(
classSymbol: KtNamedClassOrObjectSymbol, classSymbol: KtNamedClassOrObjectSymbol,
conditions: List<KtWhenCondition>, conditions: List<KtWhenCondition>,
visibilityChecker: CompletionVisibilityChecker visibilityChecker: CompletionVisibilityChecker,
isSingleCondition: Boolean,
) { ) {
require(classSymbol.classKind == KtClassKind.ENUM_CLASS) require(classSymbol.classKind == KtClassKind.ENUM_CLASS)
val handledCasesNames = conditions.mapNotNullTo(hashSetOf()) { condition -> val handledCasesNames = conditions.mapNotNullTo(hashSetOf()) { condition ->
@@ -179,18 +201,33 @@ internal class FirWhenWithSubjectConditionContributor(
"${classSymbol.name.asString()}.${entry.name.asString()}", "${classSymbol.name.asString()}.${entry.name.asString()}",
entry, entry,
entry.callableIdIfNonLocal?.asSingleFqName(), entry.callableIdIfNonLocal?.asSingleFqName(),
isPrefixNeeded = false isPrefixNeeded = false,
isSingleCondition,
) )
} }
} }
private fun KtAnalysisSession.addLookupElement(lookupString: String, symbol: KtNamedSymbol, fqName: FqName?, isPrefixNeeded: Boolean) { private fun KtWhenCondition.isSingleConditionInEntry(): Boolean {
val lookupObject = WhenConditionLookupObject(symbol.name, fqName, isPrefixNeeded) val entry = parent as KtWhenEntry
return entry.conditions.size == 1
}
private fun KtAnalysisSession.addLookupElement(
lookupString: String,
symbol: KtNamedSymbol,
fqName: FqName?,
isPrefixNeeded: Boolean,
isSingleCondition: Boolean
) {
val typeArgumentsCount = (symbol as? KtSymbolWithTypeParameters)?.typeParameters?.size ?: 0
val lookupObject = WhenConditionLookupObject(symbol.name, fqName, isPrefixNeeded, isSingleCondition, typeArgumentsCount)
LookupElementBuilder.create(lookupObject, getIsPrefix(isPrefixNeeded) + lookupString) LookupElementBuilder.create(lookupObject, getIsPrefix(isPrefixNeeded) + lookupString)
.withIcon(getIconFor(symbol)) .withIcon(getIconFor(symbol))
.withPsiElement(symbol.psi) .withPsiElement(symbol.psi)
.withInsertHandler(WhenConditionInsertionHandler) .withInsertHandler(WhenConditionInsertionHandler)
.withTailText(createStarTypeArgumentsList(typeArgumentsCount), /*grayed*/true)
.letIf(isSingleCondition) { it.appendTailText(" -> ", /*grayed*/true) }
.let(sink::addElement) .let(sink::addElement)
} }
} }
@@ -199,20 +236,41 @@ private data class WhenConditionLookupObject(
override val shortName: Name, override val shortName: Name,
val fqName: FqName?, val fqName: FqName?,
val needIsPrefix: Boolean, val needIsPrefix: Boolean,
val isSingleCondition: Boolean,
val typeArgumentsCount: Int,
) : KotlinLookupObject ) : KotlinLookupObject
private object WhenConditionInsertionHandler : InsertionHandlerBase<WhenConditionLookupObject>(WhenConditionLookupObject::class) { private object WhenConditionInsertionHandler : InsertionHandlerBase<WhenConditionLookupObject>(WhenConditionLookupObject::class) {
override fun handleInsert(context: InsertionContext, item: LookupElement, ktFile: KtFile, lookupObject: WhenConditionLookupObject) { override fun handleInsert(context: InsertionContext, item: LookupElement, ktFile: KtFile, lookupObject: WhenConditionLookupObject) {
context.insertName(lookupObject, ktFile)
context.addTypeArguments(lookupObject.typeArgumentsCount)
context.addArrow(lookupObject)
}
private fun InsertionContext.addArrow(
lookupObject: WhenConditionLookupObject
) {
if (lookupObject.isSingleCondition && completionChar != ',') {
addSymbolToCompletion(" -> ")
commitDocument()
}
}
private fun InsertionContext.insertName(
lookupObject: WhenConditionLookupObject,
ktFile: KtFile
) {
if (lookupObject.fqName != null) { if (lookupObject.fqName != null) {
val fqName = lookupObject.fqName val fqName = lookupObject.fqName
context.document.replaceString( document.replaceString(
context.startOffset, startOffset,
context.tailOffset, tailOffset,
getIsPrefix(lookupObject.needIsPrefix) + fqName.render() getIsPrefix(lookupObject.needIsPrefix) + fqName.render()
) )
context.commitDocument() commitDocument()
shortenReferencesForFirCompletion(ktFile, TextRange(context.startOffset, context.tailOffset)) shortenReferencesForFirCompletion(ktFile, TextRange(startOffset, tailOffset))
} }
} }
} }
@@ -9,9 +9,10 @@ import com.intellij.codeInsight.completion.CodeCompletionHandlerBase
import com.intellij.codeInsight.completion.CompletionType import com.intellij.codeInsight.completion.CompletionType
import com.intellij.codeInsight.completion.InsertionContext import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.psi.PsiDocumentManager
internal fun InsertionContext.addSymbolAndInvokeCompletion(symbol: String) { internal fun InsertionContext.addSymbolAndInvokeCompletion(symbol: String) {
addSymbolToCompletion(this, symbol) this.addSymbolToCompletion(symbol)
invokeCompletion(this) invokeCompletion(this)
} }
@@ -24,8 +25,24 @@ private fun invokeCompletion(context: InsertionContext) {
} }
} }
private fun addSymbolToCompletion(context: InsertionContext, symbol: String) { internal fun InsertionContext.addSymbolToCompletion(symbol: String) {
context.document.insertString(context.tailOffset, symbol) PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document)
context.commitDocument() document.insertString(tailOffset, symbol)
context.editor.caretModel.moveToOffset(context.tailOffset) commitDocument()
editor.caretModel.moveToOffset(tailOffset)
}
internal fun InsertionContext.addTypeArguments(typeArgumentsCount: Int) {
when {
typeArgumentsCount == 0 -> {
return
}
typeArgumentsCount < 0 -> {
error("Count of type arguments should be non-negative, but was $typeArgumentsCount")
}
else -> {
commitDocument()
addSymbolToCompletion(createStarTypeArgumentsList(typeArgumentsCount))
}
}
} }
@@ -0,0 +1,13 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.completion.contributors.helpers
internal fun createStarTypeArgumentsList(typeArgumentsCount: Int): String =
if (typeArgumentsCount > 0) {
List(typeArgumentsCount) { "*" }.joinToString(prefix = "<", postfix = ">")
} else {
""
}
@@ -0,0 +1,16 @@
class NON_SEALED
class AAAA<E, S>: NON_SEALED()
object BBBB: NON_SEALED()
class CCCC<E>: NON_SEALED()
fun foo(e: NON_SEALED) {
when (e) {
<caret>
}
}
// EXIST: { lookupString: "is AAAA", tailText: "<*, *> -> " }
// EXIST: BBBB
// EXIST: { lookupString: "is CCCC", tailText: "<*> -> " }
// EXIST: { lookupString: "else -> "}
// FIR_COMPARISON
@@ -0,0 +1,17 @@
sealed class SEALED
class AAAA<E, S>: SEALED()
object BBBB: SEALED()
class CCCC<E>: SEALED()
fun foo(e: SEALED) {
when (e) {
<caret>
}
}
// EXIST: { lookupString: "is AAAA", tailText: "<*, *> -> " }
// EXIST: BBBB
// EXIST: { lookupString: "is CCCC", tailText: "<*> -> " }
// EXIST: { lookupString: "else -> "}
// NOTHING_ELSE
// FIR_COMPARISON
@@ -3382,6 +3382,11 @@ public class HighLevelJvmBasicCompletionTestGenerated extends AbstractHighLevelJ
runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/nonSealedClassInWhen.kt"); runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/nonSealedClassInWhen.kt");
} }
@TestMetadata("nonSealedClassWithTypeArgs.kt")
public void testNonSealedClassWithTypeArgs() throws Exception {
runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/nonSealedClassWithTypeArgs.kt");
}
@TestMetadata("nullableEnumInWhen.kt") @TestMetadata("nullableEnumInWhen.kt")
public void testNullableEnumInWhen() throws Exception { public void testNullableEnumInWhen() throws Exception {
runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/nullableEnumInWhen.kt"); runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/nullableEnumInWhen.kt");
@@ -3407,6 +3412,11 @@ public class HighLevelJvmBasicCompletionTestGenerated extends AbstractHighLevelJ
runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/sealedClassWithAbstractInheritorsInWhen.kt"); runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/sealedClassWithAbstractInheritorsInWhen.kt");
} }
@TestMetadata("sealedClassWithTypeArgs.kt")
public void testSealedClassWithTypeArgs() throws Exception {
runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/sealedClassWithTypeArgs.kt");
}
@TestMetadata("sealedInterfaceInWhen.kt") @TestMetadata("sealedInterfaceInWhen.kt")
public void testSealedInterfaceInWhen() throws Exception { public void testSealedInterfaceInWhen() throws Exception {
runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/sealedInterfaceInWhen.kt"); runTest("idea/idea-completion/testData/../../idea-fir/testData/completion/basic/common/when/sealedInterfaceInWhen.kt");
@@ -65,10 +65,13 @@ abstract class CompletionDummyIdentifierProviderService {
?: specialInTypeArgsDummyIdentifier(tokenBefore) ?: specialInTypeArgsDummyIdentifier(tokenBefore)
?: specialInArgumentListDummyIdentifier(tokenBefore) ?: specialInArgumentListDummyIdentifier(tokenBefore)
?: isInTypeParametersList(tokenBefore) ?: isInTypeParametersList(tokenBefore)
?: handleDefaultCase(context)
?: DEFAULT_DUMMY_IDENTIFIER ?: DEFAULT_DUMMY_IDENTIFIER
} }
} }
protected open fun handleDefaultCase(context: CompletionInitializationContext): String? = null
private fun isInTypeParametersList(tokenBefore: PsiElement?): String? { private fun isInTypeParametersList(tokenBefore: PsiElement?): String? {
if (tokenBefore == null) return null if (tokenBefore == null) return null
if (tokenBefore.parents.any { it is KtTypeParameterList }) { if (tokenBefore.parents.any { it is KtTypeParameterList }) {