From 0739f6974fd2a9442a020c2157b99c171b9af05d Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Tue, 18 May 2021 14:35:44 +0200 Subject: [PATCH] FIR IDE: fix completion of type arguments without closing > --- ...ompletionDummyIdentifierProviderService.kt | 22 ++ .../completion/KotlinCompletionContributor.kt | 187 +-------------- .../common/GenericExtensionFunReceiver1.kt | 1 + .../common/GenericExtensionFunReceiver2.kt | 1 + .../common/NestedClassNameForExtension.kt | 1 + .../common/NestedClassNameForExtension2.kt | 1 + .../common/annotations/AnnotationTarget.kt | 1 + .../AutopopupInFunExtensionReceiver.kt | 1 + .../GenericExtensionFunTypeArgument.kt | 1 + .../GenericExtensionFunTypeArgument2.kt | 1 + .../basic/common/autoPopup/InExtFunName.kt | 1 + .../common/autoPopup/InGenericExtFunName.kt | 1 + .../basic/common/autoPopup/InValExtType.kt | 1 + .../typeArgsOrNot/ConstructorTypeArg.kt | 1 + .../common/typeArgsOrNot/FunctionTypeArg.kt | 1 + .../common/typeArgsOrNot/FunctionTypeArg2.kt | 1 + .../basic/common/typeArgsOrNot/LessThan.kt | 1 - .../common/typeArgsOrNot/NestedTypeArg.kt | 1 + .../common/typeArgsOrNot/SecondTypeArg.kt | 1 + .../common/typeArgsOrNot/SecondTypeArg3.kt | 1 + ...ompletionDummyIdentifierProviderService.kt | 27 +++ .../KotlinFirCompletionContributor.kt | 9 + ...ompletionDummyIdentifierProviderService.kt | 215 ++++++++++++++++++ .../resources-descriptors/META-INF/plugin.xml | 3 + idea/resources-fir/META-INF/plugin.xml | 2 + 25 files changed, 300 insertions(+), 183 deletions(-) create mode 100644 idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/FE10CompletionDummyIdentifierProviderService.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/FirCompletionDummyIdentifierProviderService.kt create mode 100644 idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/completion/CompletionDummyIdentifierProviderService.kt diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/FE10CompletionDummyIdentifierProviderService.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/FE10CompletionDummyIdentifierProviderService.kt new file mode 100644 index 00000000000..ba7060b61e4 --- /dev/null +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/FE10CompletionDummyIdentifierProviderService.kt @@ -0,0 +1,22 @@ +/* + * 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 + +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +class FE10CompletionDummyIdentifierProviderService: CompletionDummyIdentifierProviderService() { + override fun allTargetsAreFunctionsOrClasses(nameReferenceExpression: KtNameReferenceExpression): Boolean { + val bindingContext = nameReferenceExpression.getResolutionFacade().analyze(nameReferenceExpression, BodyResolveMode.PARTIAL) + val targets = nameReferenceExpression.getReferenceTargets(bindingContext) + return targets.isNotEmpty() && targets.all { it is FunctionDescriptor || it is ClassDescriptor && it.kind == ClassKind.CLASS } + } +} \ No newline at end of file diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KotlinCompletionContributor.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KotlinCompletionContributor.kt index 9ef3fba0414..ced95e3e633 100644 --- a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KotlinCompletionContributor.kt +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KotlinCompletionContributor.kt @@ -7,22 +7,15 @@ package org.jetbrains.kotlin.idea.completion import com.intellij.codeInsight.completion.* import com.intellij.codeInsight.lookup.LookupElement -import com.intellij.codeInsight.lookup.LookupElementDecorator +import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.util.Key import com.intellij.patterns.PlatformPatterns import com.intellij.patterns.PsiJavaPatterns.elementType import com.intellij.patterns.PsiJavaPatterns.psiElement -import com.intellij.psi.* -import com.intellij.psi.search.PsiElementProcessor -import com.intellij.psi.tree.TokenSet -import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.PsiComment import com.intellij.util.ProcessingContext -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.ClassKind -import org.jetbrains.kotlin.descriptors.FunctionDescriptor -import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade import org.jetbrains.kotlin.idea.completion.smart.SmartCompletion import org.jetbrains.kotlin.idea.completion.smart.SmartCompletionSession import org.jetbrains.kotlin.idea.statistics.CompletionFUSCollector.completionStatsData @@ -30,10 +23,8 @@ import org.jetbrains.kotlin.idea.statistics.CompletionTypeStats import org.jetbrains.kotlin.idea.statistics.FileTypeStats import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.* -import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets -import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode -import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType import kotlin.math.max var KtFile.doNotComplete: Boolean? by UserDataProperty(Key.create("DO_NOT_COMPLETE")) @@ -91,17 +82,7 @@ class KotlinCompletionContributor : CompletionContributor() { PackageDirectiveCompletion.ACTIVATION_PATTERN.accepts(tokenBefore) -> PackageDirectiveCompletion.DUMMY_IDENTIFIER - isInClassHeader(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER // do not add '$' to not interrupt class declaration parsing - - isInUnclosedSuperQualifier(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + ">" - - isInSimpleStringTemplate(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED - - else -> specialLambdaSignatureDummyIdentifier(tokenBefore) - ?: specialExtensionReceiverDummyIdentifier(tokenBefore) - ?: specialInTypeArgsDummyIdentifier(tokenBefore) - ?: specialInArgumentListDummyIdentifier(tokenBefore) - ?: DEFAULT_DUMMY_IDENTIFIER + else -> service().provideDummyIdentifier(context) } val tokenAt = psiFile.findElementAt(max(0, offset)) @@ -166,76 +147,6 @@ class KotlinCompletionContributor : CompletionContributor() { return expression.textRange!!.endOffset } - private fun isInClassHeader(tokenBefore: PsiElement?): Boolean { - val classOrObject = tokenBefore?.parents?.firstIsInstanceOrNull() ?: return false - val name = classOrObject.nameIdentifier ?: return false - val headerEnd = classOrObject.body?.startOffset ?: classOrObject.endOffset - val offset = tokenBefore.startOffset - return name.endOffset <= offset && offset <= headerEnd - } - - private fun specialLambdaSignatureDummyIdentifier(tokenBefore: PsiElement?): String? { - var leaf = tokenBefore - while (leaf is PsiWhiteSpace || leaf is PsiComment) { - leaf = leaf.prevLeaf(true) - } - - val lambda = leaf?.parents?.firstOrNull { it is KtFunctionLiteral } ?: return null - - val lambdaChild = leaf.parents.takeWhile { it != lambda }.lastOrNull() - - return if (lambdaChild is KtParameterList) - CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED - else - null - - } - - private val declarationKeywords = TokenSet.create(KtTokens.FUN_KEYWORD, KtTokens.VAL_KEYWORD, KtTokens.VAR_KEYWORD) - private val declarationTokens = TokenSet.orSet( - TokenSet.create( - KtTokens.IDENTIFIER, KtTokens.LT, KtTokens.GT, - KtTokens.COMMA, KtTokens.DOT, KtTokens.QUEST, KtTokens.COLON, - KtTokens.IN_KEYWORD, KtTokens.OUT_KEYWORD, - KtTokens.LPAR, KtTokens.RPAR, KtTokens.ARROW, - TokenType.ERROR_ELEMENT - ), - KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET - ) - - private fun specialExtensionReceiverDummyIdentifier(tokenBefore: PsiElement?): String? { - var token = tokenBefore ?: return null - var ltCount = 0 - var gtCount = 0 - val builder = StringBuilder() - while (true) { - val tokenType = token.node!!.elementType - if (tokenType in declarationKeywords) { - val balance = ltCount - gtCount - if (balance < 0) return null - builder.append(token.text!!.reversed()) - builder.reverse() - - var tail = "X" + ">".repeat(balance) + ".f" - if (tokenType == KtTokens.FUN_KEYWORD) { - tail += "()" - } - builder.append(tail) - - val text = builder.toString() - val file = KtPsiFactory(tokenBefore.project).createFile(text) - val declaration = file.declarations.singleOrNull() ?: return null - if (declaration.textLength != text.length) return null - val containsErrorElement = !PsiTreeUtil.processElements(file, PsiElementProcessor { it !is PsiErrorElement }) - return if (containsErrorElement) null else "$tail$" - } - if (tokenType !in declarationTokens) return null - if (tokenType == KtTokens.LT) ltCount++ - if (tokenType == KtTokens.GT) gtCount++ - builder.append(token.text!!.reversed()) - token = PsiTreeUtil.prevLeaf(token) ?: return null - } - } private fun performCompletion(parameters: CompletionParameters, result: CompletionResultSet) { val position = parameters.position @@ -402,94 +313,6 @@ class KotlinCompletionContributor : CompletionContributor() { } return true } - - private fun specialInTypeArgsDummyIdentifier(tokenBefore: PsiElement?): String? { - if (tokenBefore == null) return null - - if (tokenBefore.getParentOfType(true) != null) { // already parsed inside type argument list - return CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED // do not insert '$' to not break type argument list parsing - } - - val pair = unclosedTypeArgListNameAndBalance(tokenBefore) ?: return null - val (nameToken, balance) = pair - assert(balance > 0) - - val nameRef = nameToken.parent as? KtNameReferenceExpression ?: return null - val bindingContext = nameRef.getResolutionFacade().analyze(nameRef, BodyResolveMode.PARTIAL) - val targets = nameRef.getReferenceTargets(bindingContext) - return if (targets.isNotEmpty() && targets.all { it is FunctionDescriptor || it is ClassDescriptor && it.kind == ClassKind.CLASS }) { - CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + ">".repeat(balance) + "$" - } else { - null - } - } - - private fun unclosedTypeArgListNameAndBalance(tokenBefore: PsiElement): Pair? { - val nameToken = findCallNameTokenIfInTypeArgs(tokenBefore) ?: return null - val pair = unclosedTypeArgListNameAndBalance(nameToken) - return if (pair == null) { - Pair(nameToken, 1) - } else { - Pair(pair.first, pair.second + 1) - } - } - - private val callTypeArgsTokens = TokenSet.orSet( - TokenSet.create( - KtTokens.IDENTIFIER, KtTokens.LT, KtTokens.GT, - KtTokens.COMMA, KtTokens.DOT, KtTokens.QUEST, KtTokens.COLON, - KtTokens.LPAR, KtTokens.RPAR, KtTokens.ARROW - ), - KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET - ) - - // if the leaf could be located inside type argument list of a call (if parsed properly) - // then it returns the call name reference this type argument list would belong to - private fun findCallNameTokenIfInTypeArgs(leaf: PsiElement): PsiElement? { - var current = leaf - while (true) { - val tokenType = current.node!!.elementType - if (tokenType !in callTypeArgsTokens) return null - - if (tokenType == KtTokens.LT) { - val nameToken = current.prevLeaf(skipEmptyElements = true) ?: return null - if (nameToken.node!!.elementType != KtTokens.IDENTIFIER) return null - return nameToken - } - - if (tokenType == KtTokens.GT) { // pass nested type argument list - val prev = current.prevLeaf(skipEmptyElements = true) ?: return null - val typeRef = findCallNameTokenIfInTypeArgs(prev) ?: return null - current = typeRef - continue - } - - current = current.prevLeaf(skipEmptyElements = true) ?: return null - } - } - - private fun specialInArgumentListDummyIdentifier(tokenBefore: PsiElement?): String? { - // If we insert $ in the argument list of a delegation specifier, this will break parsing - // and the following block will not be attached as a body to the constructor. Therefore - // we need to use a regular identifier. - val argumentList = tokenBefore?.getNonStrictParentOfType() ?: return null - if (argumentList.parent is KtConstructorDelegationCall) return CompletionUtil.DUMMY_IDENTIFIER_TRIMMED - return null - } - - private fun isInUnclosedSuperQualifier(tokenBefore: PsiElement?): Boolean { - if (tokenBefore == null) return false - val tokensToSkip = TokenSet.orSet(TokenSet.create(KtTokens.IDENTIFIER, KtTokens.DOT), KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET) - val tokens = generateSequence(tokenBefore) { it.prevLeaf() } - val ltToken = tokens.firstOrNull { it.node.elementType !in tokensToSkip } ?: return false - if (ltToken.node.elementType != KtTokens.LT) return false - val superToken = ltToken.prevLeaf { it !is PsiWhiteSpace && it !is PsiComment } - return superToken?.node?.elementType == KtTokens.SUPER_KEYWORD - } - - private fun isInSimpleStringTemplate(tokenBefore: PsiElement?): Boolean { - return tokenBefore?.parents?.firstIsInstanceOrNull()?.isPlain() ?: false - } } abstract class KotlinCompletionExtension { diff --git a/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver1.kt b/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver1.kt index 570a5eb66a1..36f68983549 100644 --- a/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver1.kt +++ b/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver1.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun foo() {} fun S diff --git a/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver2.kt b/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver2.kt index 0668a823c0b..4faaa150a9f 100644 --- a/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver2.kt +++ b/idea/idea-completion/testData/basic/common/GenericExtensionFunReceiver2.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Foo{} fun foo() {} diff --git a/idea/idea-completion/testData/basic/common/NestedClassNameForExtension.kt b/idea/idea-completion/testData/basic/common/NestedClassNameForExtension.kt index 2941f4661d9..9b9a46fec27 100644 --- a/idea/idea-completion/testData/basic/common/NestedClassNameForExtension.kt +++ b/idea/idea-completion/testData/basic/common/NestedClassNameForExtension.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Test { public class Nested } diff --git a/idea/idea-completion/testData/basic/common/NestedClassNameForExtension2.kt b/idea/idea-completion/testData/basic/common/NestedClassNameForExtension2.kt index 581dff6db41..96560c6951d 100644 --- a/idea/idea-completion/testData/basic/common/NestedClassNameForExtension2.kt +++ b/idea/idea-completion/testData/basic/common/NestedClassNameForExtension2.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Test { public class Nested } diff --git a/idea/idea-completion/testData/basic/common/annotations/AnnotationTarget.kt b/idea/idea-completion/testData/basic/common/annotations/AnnotationTarget.kt index a663b953d8c..fc131859d25 100644 --- a/idea/idea-completion/testData/basic/common/annotations/AnnotationTarget.kt +++ b/idea/idea-completion/testData/basic/common/annotations/AnnotationTarget.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON @ annotation class Annotated // EXIST: Target diff --git a/idea/idea-completion/testData/basic/common/autoPopup/AutopopupInFunExtensionReceiver.kt b/idea/idea-completion/testData/basic/common/autoPopup/AutopopupInFunExtensionReceiver.kt index 0618be50a63..fad46cc5cb9 100644 --- a/idea/idea-completion/testData/basic/common/autoPopup/AutopopupInFunExtensionReceiver.kt +++ b/idea/idea-completion/testData/basic/common/autoPopup/AutopopupInFunExtensionReceiver.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Xyz fun X diff --git a/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument.kt b/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument.kt index 37fee6a814e..392f3c37853 100644 --- a/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument.kt +++ b/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun Strange(){} fun Annotations diff --git a/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument2.kt b/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument2.kt index 2ae1c42eca3..e72a8adb06b 100644 --- a/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument2.kt +++ b/idea/idea-completion/testData/basic/common/autoPopup/GenericExtensionFunTypeArgument2.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun Strange(){} fun Map<() -> Unit, Str diff --git a/idea/idea-completion/testData/basic/common/autoPopup/InExtFunName.kt b/idea/idea-completion/testData/basic/common/autoPopup/InExtFunName.kt index a197de47724..e974f234030 100644 --- a/idea/idea-completion/testData/basic/common/autoPopup/InExtFunName.kt +++ b/idea/idea-completion/testData/basic/common/autoPopup/InExtFunName.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Outer { class Nested } diff --git a/idea/idea-completion/testData/basic/common/autoPopup/InGenericExtFunName.kt b/idea/idea-completion/testData/basic/common/autoPopup/InGenericExtFunName.kt index 3fd4c934e8a..5c5559271e7 100644 --- a/idea/idea-completion/testData/basic/common/autoPopup/InGenericExtFunName.kt +++ b/idea/idea-completion/testData/basic/common/autoPopup/InGenericExtFunName.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Outer { class Nested } diff --git a/idea/idea-completion/testData/basic/common/autoPopup/InValExtType.kt b/idea/idea-completion/testData/basic/common/autoPopup/InValExtType.kt index 2ccb8786800..a41b96f9245 100644 --- a/idea/idea-completion/testData/basic/common/autoPopup/InValExtType.kt +++ b/idea/idea-completion/testData/basic/common/autoPopup/InValExtType.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Some val S diff --git a/idea/idea-completion/testData/basic/common/typeArgsOrNot/ConstructorTypeArg.kt b/idea/idea-completion/testData/basic/common/typeArgsOrNot/ConstructorTypeArg.kt index f667b0544c4..ee7bdc9c29d 100644 --- a/idea/idea-completion/testData/basic/common/typeArgsOrNot/ConstructorTypeArg.kt +++ b/idea/idea-completion/testData/basic/common/typeArgsOrNot/ConstructorTypeArg.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON import java.util.HashMap fun foo() { diff --git a/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg.kt b/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg.kt index 64a3f112ddc..8eca9d2df6a 100644 --- a/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg.kt +++ b/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun foo() { val v = listOf< } diff --git a/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg2.kt b/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg2.kt index 3e3b26a748f..54652478fd3 100644 --- a/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg2.kt +++ b/idea/idea-completion/testData/basic/common/typeArgsOrNot/FunctionTypeArg2.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun genericFoo(p: Int){} fun genericFoo(c: Char){} diff --git a/idea/idea-completion/testData/basic/common/typeArgsOrNot/LessThan.kt b/idea/idea-completion/testData/basic/common/typeArgsOrNot/LessThan.kt index 4135dd9bdc0..564e044914b 100644 --- a/idea/idea-completion/testData/basic/common/typeArgsOrNot/LessThan.kt +++ b/idea/idea-completion/testData/basic/common/typeArgsOrNot/LessThan.kt @@ -1,4 +1,3 @@ -// FIR_COMPARISON val v = 1 fun f() = 2 diff --git a/idea/idea-completion/testData/basic/common/typeArgsOrNot/NestedTypeArg.kt b/idea/idea-completion/testData/basic/common/typeArgsOrNot/NestedTypeArg.kt index cb1a680a3ab..6e683c75ab6 100644 --- a/idea/idea-completion/testData/basic/common/typeArgsOrNot/NestedTypeArg.kt +++ b/idea/idea-completion/testData/basic/common/typeArgsOrNot/NestedTypeArg.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun foo() { val v = HashMap Unit>, Set< } diff --git a/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg.kt b/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg.kt index 401e9b15a03..53ad9c176b7 100644 --- a/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg.kt +++ b/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun foo() { val v = HashMap } diff --git a/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg3.kt b/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg3.kt index e93dc89c449..29c5fe61fe1 100644 --- a/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg3.kt +++ b/idea/idea-completion/testData/basic/common/typeArgsOrNot/SecondTypeArg3.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON import java.util.HashMap fun foo() { diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/FirCompletionDummyIdentifierProviderService.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/FirCompletionDummyIdentifierProviderService.kt new file mode 100644 index 00000000000..db23add4230 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/FirCompletionDummyIdentifierProviderService.kt @@ -0,0 +1,27 @@ +/* + * 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 + +import org.jetbrains.kotlin.idea.frontend.api.analyse +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassKind +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassOrObjectSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionSymbol +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.psi.KtNameReferenceExpression + +class FirCompletionDummyIdentifierProviderService : CompletionDummyIdentifierProviderService() { + override fun allTargetsAreFunctionsOrClasses(nameReferenceExpression: KtNameReferenceExpression): Boolean { + return true + // TODO fir cannot handle invalid code and handles listOf< as binary expression +// return analyse(nameReferenceExpression) { +// val reference = nameReferenceExpression.mainReference +// val targets = reference.resolveToSymbols() +// targets.isNotEmpty() && targets.all { target -> +// target is KtFunctionSymbol || target is KtClassOrObjectSymbol && target.classKind == KtClassKind.CLASS +// } +// } + } +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt index e5b0a8e871b..87c7e124d18 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.idea.completion import com.intellij.codeInsight.completion.* +import com.intellij.openapi.components.service import com.intellij.patterns.PlatformPatterns import com.intellij.patterns.PsiJavaPatterns import com.intellij.util.ProcessingContext @@ -28,11 +29,19 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.IndexHelper import org.jetbrains.kotlin.idea.fir.low.level.api.util.originalKtFile import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtFile class KotlinFirCompletionContributor : CompletionContributor() { init { extend(CompletionType.BASIC, PlatformPatterns.psiElement(), KotlinFirCompletionProvider) } + + override fun beforeCompletion(context: CompletionInitializationContext) { + val psiFile = context.file + if (psiFile !is KtFile) return + + context.dummyIdentifier = service().provideDummyIdentifier(context) + } } private object KotlinFirCompletionProvider : CompletionProvider() { diff --git a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/completion/CompletionDummyIdentifierProviderService.kt b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/completion/CompletionDummyIdentifierProviderService.kt new file mode 100644 index 00000000000..79fbe53ba39 --- /dev/null +++ b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/completion/CompletionDummyIdentifierProviderService.kt @@ -0,0 +1,215 @@ +/* + * 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 + +import com.intellij.codeInsight.completion.CompletionInitializationContext +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.completion.CompletionUtil +import com.intellij.codeInsight.completion.CompletionUtilCore +import com.intellij.psi.* +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import kotlin.math.max + +abstract class CompletionDummyIdentifierProviderService { + fun provideDummyIdentifier(context: CompletionInitializationContext): String { + val psiFile = context.file + if (psiFile !is KtFile) { + error("CompletionDummyIdentifierProviderService.providerDummyIdentifier should not be called for non KtFile") + } + + val offset = context.startOffset + val tokenBefore = psiFile.findElementAt(max(0, offset - 1)) + + return when { + context.completionType == CompletionType.SMART -> DEFAULT_DUMMY_IDENTIFIER + + // TODO package completion + + isInClassHeader(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER // do not add '$' to not interrupt class declaration parsing + + isInUnclosedSuperQualifier(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + ">" + + isInSimpleStringTemplate(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + + else -> specialLambdaSignatureDummyIdentifier(tokenBefore) + ?: specialExtensionReceiverDummyIdentifier(tokenBefore) + ?: specialInTypeArgsDummyIdentifier(tokenBefore) + ?: specialInArgumentListDummyIdentifier(tokenBefore) + ?: DEFAULT_DUMMY_IDENTIFIER + } + } + + private fun specialLambdaSignatureDummyIdentifier(tokenBefore: PsiElement?): String? { + var leaf = tokenBefore + while (leaf is PsiWhiteSpace || leaf is PsiComment) { + leaf = leaf.prevLeaf(true) + } + + val lambda = leaf?.parents?.firstOrNull { it is KtFunctionLiteral } ?: return null + + val lambdaChild = leaf.parents.takeWhile { it != lambda }.lastOrNull() + + return if (lambdaChild is KtParameterList) + CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + else + null + + } + + private fun isInClassHeader(tokenBefore: PsiElement?): Boolean { + val classOrObject = tokenBefore?.parents?.firstIsInstanceOrNull() ?: return false + val name = classOrObject.nameIdentifier ?: return false + val headerEnd = classOrObject.body?.startOffset ?: classOrObject.endOffset + val offset = tokenBefore.startOffset + return name.endOffset <= offset && offset <= headerEnd + } + + private fun isInUnclosedSuperQualifier(tokenBefore: PsiElement?): Boolean { + if (tokenBefore == null) return false + val tokensToSkip = TokenSet.orSet(TokenSet.create(KtTokens.IDENTIFIER, KtTokens.DOT), KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET) + val tokens = generateSequence(tokenBefore) { it.prevLeaf() } + val ltToken = tokens.firstOrNull { it.node.elementType !in tokensToSkip } ?: return false + if (ltToken.node.elementType != KtTokens.LT) return false + val superToken = ltToken.prevLeaf { it !is PsiWhiteSpace && it !is PsiComment } + return superToken?.node?.elementType == KtTokens.SUPER_KEYWORD + } + + private fun isInSimpleStringTemplate(tokenBefore: PsiElement?): Boolean { + return tokenBefore?.parents?.firstIsInstanceOrNull()?.isPlain() ?: false + } + + + private fun specialExtensionReceiverDummyIdentifier(tokenBefore: PsiElement?): String? { + var token = tokenBefore ?: return null + var ltCount = 0 + var gtCount = 0 + val builder = StringBuilder() + while (true) { + val tokenType = token.node!!.elementType + if (tokenType in declarationKeywords) { + val balance = ltCount - gtCount + if (balance < 0) return null + builder.append(token.text!!.reversed()) + builder.reverse() + + var tail = "X" + ">".repeat(balance) + ".f" + if (tokenType == KtTokens.FUN_KEYWORD) { + tail += "()" + } + builder.append(tail) + + val text = builder.toString() + val file = KtPsiFactory(tokenBefore.project).createFile(text) + val declaration = file.declarations.singleOrNull() ?: return null + if (declaration.textLength != text.length) return null + val containsErrorElement = !PsiTreeUtil.processElements(file) { it !is PsiErrorElement } + return if (containsErrorElement) null else "$tail$" + } + if (tokenType !in declarationTokens) return null + if (tokenType == KtTokens.LT) ltCount++ + if (tokenType == KtTokens.GT) gtCount++ + builder.append(token.text!!.reversed()) + token = PsiTreeUtil.prevLeaf(token) ?: return null + } + } + + private fun specialInTypeArgsDummyIdentifier(tokenBefore: PsiElement?): String? { + if (tokenBefore == null) return null + + if (tokenBefore.getParentOfType(true) != null) { // already parsed inside type argument list + return CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED // do not insert '$' to not break type argument list parsing + } + + val pair = unclosedTypeArgListNameAndBalance(tokenBefore) ?: return null + val (nameToken, balance) = pair + assert(balance > 0) + + val nameRef = nameToken.parent as? KtNameReferenceExpression ?: return null + return if (allTargetsAreFunctionsOrClasses(nameRef)) { + CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + ">".repeat(balance) + "$" + } else { + null + } + } + + protected abstract fun allTargetsAreFunctionsOrClasses(nameReferenceExpression: KtNameReferenceExpression): Boolean + + private fun unclosedTypeArgListNameAndBalance(tokenBefore: PsiElement): Pair? { + val nameToken = findCallNameTokenIfInTypeArgs(tokenBefore) ?: return null + val pair = unclosedTypeArgListNameAndBalance(nameToken) + return if (pair == null) { + Pair(nameToken, 1) + } else { + Pair(pair.first, pair.second + 1) + } + } + + private val callTypeArgsTokens = TokenSet.orSet( + TokenSet.create( + KtTokens.IDENTIFIER, KtTokens.LT, KtTokens.GT, + KtTokens.COMMA, KtTokens.DOT, KtTokens.QUEST, KtTokens.COLON, + KtTokens.LPAR, KtTokens.RPAR, KtTokens.ARROW + ), + KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET + ) + + // if the leaf could be located inside type argument list of a call (if parsed properly) + // then it returns the call name reference this type argument list would belong to + private fun findCallNameTokenIfInTypeArgs(leaf: PsiElement): PsiElement? { + var current = leaf + while (true) { + val tokenType = current.node!!.elementType + if (tokenType !in callTypeArgsTokens) return null + + if (tokenType == KtTokens.LT) { + val nameToken = current.prevLeaf(skipEmptyElements = true) ?: return null + if (nameToken.node!!.elementType != KtTokens.IDENTIFIER) return null + return nameToken + } + + if (tokenType == KtTokens.GT) { // pass nested type argument list + val prev = current.prevLeaf(skipEmptyElements = true) ?: return null + val typeRef = findCallNameTokenIfInTypeArgs(prev) ?: return null + current = typeRef + continue + } + + current = current.prevLeaf(skipEmptyElements = true) ?: return null + } + } + + + private fun specialInArgumentListDummyIdentifier(tokenBefore: PsiElement?): String? { + // If we insert $ in the argument list of a delegation specifier, this will break parsing + // and the following block will not be attached as a body to the constructor. Therefore + // we need to use a regular identifier. + val argumentList = tokenBefore?.getNonStrictParentOfType() ?: return null + if (argumentList.parent is KtConstructorDelegationCall) return CompletionUtil.DUMMY_IDENTIFIER_TRIMMED + return null + } + + companion object { + val DEFAULT_DUMMY_IDENTIFIER: String = + CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + "$" // add '$' to ignore context after the caret + + private val declarationKeywords = TokenSet.create(KtTokens.FUN_KEYWORD, KtTokens.VAL_KEYWORD, KtTokens.VAR_KEYWORD) + private val declarationTokens = TokenSet.orSet( + TokenSet.create( + KtTokens.IDENTIFIER, KtTokens.LT, KtTokens.GT, + KtTokens.COMMA, KtTokens.DOT, KtTokens.QUEST, KtTokens.COLON, + KtTokens.IN_KEYWORD, KtTokens.OUT_KEYWORD, + KtTokens.LPAR, KtTokens.RPAR, KtTokens.ARROW, + TokenType.ERROR_ELEMENT + ), + KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET + ) + } +} diff --git a/idea/resources-descriptors/META-INF/plugin.xml b/idea/resources-descriptors/META-INF/plugin.xml index 210e202dace..d02d993e18d 100644 --- a/idea/resources-descriptors/META-INF/plugin.xml +++ b/idea/resources-descriptors/META-INF/plugin.xml @@ -156,6 +156,9 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. + + +