[kotlin] KTIJ-34546 - fixed template completion results being duplicated with chain completion

^KTIJ-34546 fixed


Merge-request: IJ-MR-166132
Merged-by: Frederik Haselmeier <Frederik.Haselmeier@jetbrains.com>

GitOrigin-RevId: 3744ee428cb1202cd37f37828bb5cd79b2d3f40b
This commit is contained in:
Frederik Haselmeier
2025-06-26 18:37:20 +00:00
committed by intellij-monorepo-bot
parent 7a1ba51253
commit 1bb53c25f2
9 changed files with 170 additions and 112 deletions
@@ -3,7 +3,9 @@ package org.jetbrains.kotlin.idea.completion.impl.k2
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.openapi.util.registry.RegistryManager
import com.intellij.psi.PsiErrorElement
import org.jetbrains.kotlin.analysis.api.KaImplementationDetail
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.fir.diagnostics.KaFirDiagnostic
@@ -16,15 +18,18 @@ import org.jetbrains.kotlin.idea.base.projectStructure.languageVersionSettings
import org.jetbrains.kotlin.idea.completion.KotlinFirCompletionParameters
import org.jetbrains.kotlin.idea.completion.findValueArgument
import org.jetbrains.kotlin.idea.completion.impl.k2.contributors.*
import org.jetbrains.kotlin.idea.completion.lookups.ImportStrategy
import org.jetbrains.kotlin.idea.completion.lookups.factories.ClassifierLookupObject
import org.jetbrains.kotlin.idea.completion.weighers.WeighingContext
import org.jetbrains.kotlin.idea.completion.weighers.WeighingContext.Companion.getAnnotationLiteralExpectedType
import org.jetbrains.kotlin.idea.completion.weighers.WeighingContext.Companion.getEqualityExpectedType
import org.jetbrains.kotlin.idea.util.positionContext.*
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.renderer.render
import java.util.concurrent.CopyOnWriteArrayList
internal object Completions {
fun complete(
parameters: KotlinFirCompletionParameters,
positionContext: KotlinRawPositionContext,
@@ -60,11 +65,17 @@ internal object Completions {
else -> WeighingContext.create(parameters, elementInCompletionFile = positionContext.position)
}
complete(
positionContext = positionContext,
sink = LookupElementSink(resultSet, parameters),
weighingContext = weighingContext,
)
val contributors = CopyOnWriteArrayList<ChainCompletionContributor>() // if needed for the multithreaded completion
val sink = LookupElementSink(resultSet, parameters) {
contributors.addIfAbsent(it)
}
complete(positionContext, sink, weighingContext)
if (positionContext is KotlinNameReferencePositionContext
&& contributors.isNotEmpty()
&& RegistryManager.getInstance().`is`("kotlin.k2.chain.completion.enabled")) {
runChainCompletion(positionContext, sink, contributors)
}
} finally {
after()
}
@@ -260,6 +271,54 @@ internal object Completions {
}
}
}
@OptIn(KaImplementationDetail::class)
private fun runChainCompletion(
positionContext: KotlinNameReferencePositionContext,
sink: LookupElementSink,
contributors: List<ChainCompletionContributor>,
) {
val explicitReceiver = positionContext.explicitReceiver ?: return
sink.runRemainingContributors(sink.parameters.delegate) { completionResult ->
val lookupElement = completionResult.lookupElement
val classifierLookupObject = lookupElement.`object` as? ClassifierLookupObject
val nameToImport = when (val importStrategy = classifierLookupObject?.importingStrategy) {
is ImportStrategy.AddImport -> importStrategy.nameToImport
is ImportStrategy.InsertFqNameAndShorten -> importStrategy.fqName
else -> null
}
if (nameToImport == null) {
sink.passResult(completionResult)
return@runRemainingContributors
}
val expression = KtPsiFactory.contextual(explicitReceiver)
.createExpression(nameToImport.render() + "." + positionContext.nameExpression.text) as KtDotQualifiedExpression
val receiverExpression = expression.receiverExpression as? KtDotQualifiedExpression
val nameExpression = expression.selectorExpression as? KtNameReferenceExpression
if (receiverExpression == null
|| nameExpression == null
) {
sink.passResult(completionResult)
return@runRemainingContributors
}
analyze(nameExpression) {
val positionContext = KotlinExpressionNameReferencePositionContext(nameExpression)
val importingStrategy = ImportStrategy.AddImport(nameToImport)
val lookupElements = contributors.asSequence()
.flatMap { contributor ->
contributor.createChainedLookupElements(positionContext, receiverExpression, importingStrategy)
}.asIterable()
sink.addAllElements(lookupElements)
}
}
}
}
private fun KotlinUnknownPositionContext.isAfterRangeToken(): Boolean {
@@ -11,6 +11,7 @@ import com.intellij.psi.util.elementType
import org.jetbrains.kotlin.idea.base.codeInsight.contributorClass
import org.jetbrains.kotlin.idea.base.psi.dropCurlyBracketsIfPossible
import org.jetbrains.kotlin.idea.completion.KotlinFirCompletionParameters
import org.jetbrains.kotlin.idea.completion.impl.k2.contributors.ChainCompletionContributor
import org.jetbrains.kotlin.idea.completion.impl.k2.contributors.FirCompletionContributor
import org.jetbrains.kotlin.idea.completion.implCommon.handlers.CompletionCharInsertHandler
import org.jetbrains.kotlin.idea.completion.implCommon.stringTemplates.InsertStringTemplateBracesInsertHandler
@@ -30,16 +31,17 @@ internal class LookupElementSink(
internal val parameters: KotlinFirCompletionParameters,
private val groupPriority: Int = 0,
private val contributorClass: Class<FirCompletionContributor<*>>? = null,
internal val registerChainContributor: (ChainCompletionContributor) -> Unit,
) {
val prefixMatcher: PrefixMatcher
get() = resultSet.prefixMatcher
fun withPriority(groupPriority: Int): LookupElementSink =
LookupElementSink(resultSet, parameters, groupPriority, contributorClass)
LookupElementSink(resultSet, parameters, groupPriority, contributorClass, registerChainContributor)
fun withContributorClass(contributorClass: Class<FirCompletionContributor<*>>): LookupElementSink =
LookupElementSink(resultSet, parameters, groupPriority, contributorClass)
LookupElementSink(resultSet, parameters, groupPriority, contributorClass, registerChainContributor)
fun passResult(result: CompletionResult) {
resultSet.passResult(result)
@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.completion.impl.k2.contributors
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.util.NlsSafe
import com.intellij.psi.*
import com.intellij.psi.util.childrenOfType
@@ -57,7 +58,8 @@ internal open class FirCallableCompletionContributor(
sink: LookupElementSink,
priority: Int = 0,
private val withTrailingLambda: Boolean = false, // TODO find a better solution
) : FirCompletionContributorBase<KotlinNameReferencePositionContext>(sink, priority) {
) : FirCompletionContributorBase<KotlinNameReferencePositionContext>(sink, priority),
ChainCompletionContributor {
context(KaSession)
protected open fun getImportStrategy(signature: KaCallableSignature<*>, isImportDefinitelyNotRequired: Boolean): ImportStrategy =
@@ -345,6 +347,34 @@ internal open class FirCallableCompletionContributor(
yieldAll(extensionDescriptors)
}
context(KaSession)
override fun createChainedLookupElements(
positionContext: KotlinNameReferencePositionContext,
receiverExpression: KtDotQualifiedExpression,
importingStrategy: ImportStrategy
): Sequence<LookupElement> {
val weighingContext = WeighingContext.create(parameters, positionContext)
return collectDotCompletion(
positionContext = positionContext,
scopeContext = weighingContext.scopeContext,
explicitReceiver = receiverExpression,
extensionChecker = null,
showReceiver = true,
).flatMap { callableWithMetadata ->
val signature = callableWithMetadata.signature
createCallableLookupElements(
context = weighingContext,
signature = signature,
options = callableWithMetadata.options.copy(importingStrategy = importingStrategy),
scopeKind = callableWithMetadata.scopeKind,
presentableText = callableWithMetadata.itemText,
withTrailingLambda = true,
)
}
}
context(KaSession)
protected open fun collectDotCompletion(
positionContext: KotlinNameReferencePositionContext,
@@ -374,30 +404,8 @@ internal open class FirCallableCompletionContributor(
)
}
if (showReceiver) return@sequence
runChainCompletion(positionContext, explicitReceiver) { receiverExpression,
positionContext,
importingStrategy ->
val weighingContext = WeighingContext.create(parameters, positionContext)
collectDotCompletion(
positionContext = positionContext,
scopeContext = weighingContext.scopeContext,
explicitReceiver = receiverExpression,
extensionChecker = null,
showReceiver = true,
).flatMap { callableWithMetadata ->
val signature = callableWithMetadata.signature
createCallableLookupElements(
context = weighingContext,
signature = signature,
options = callableWithMetadata.options.copy(importingStrategy),
scopeKind = callableWithMetadata.scopeKind,
presentableText = callableWithMetadata.itemText,
withTrailingLambda = true,
)
}
if (!showReceiver) {
sink.registerChainContributor(this@FirCallableCompletionContributor)
}
}
}
@@ -32,15 +32,16 @@ import org.jetbrains.kotlin.idea.completion.weighers.WeighingContext
import org.jetbrains.kotlin.idea.util.positionContext.KotlinNameReferencePositionContext
import org.jetbrains.kotlin.idea.util.positionContext.KotlinRawPositionContext
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.utils.yieldIfNotNull
import kotlin.sequences.filter
internal open class FirClassifierCompletionContributor(
sink: LookupElementSink,
priority: Int = 0,
) : FirCompletionContributorBase<KotlinNameReferencePositionContext>(sink, priority) {
) : FirCompletionContributorBase<KotlinNameReferencePositionContext>(sink, priority),
ChainCompletionContributor{
context(KaSession)
protected open fun filterClassifiers(classifierSymbol: KaClassifierSymbol): Boolean = true
@@ -77,29 +78,7 @@ internal open class FirClassifierCompletionContributor(
).map { it.applyWeighs(weighingContext, symbolWithOrigin) }
}.forEach(sink::addElement)
} else {
runChainCompletion(positionContext, explicitReceiver) { receiverExpression,
positionContext,
importingStrategy ->
val selectorExpression = receiverExpression.selectorExpression
?: return@runChainCompletion emptySequence()
val reference = receiverExpression.reference()
?: return@runChainCompletion emptySequence()
val weighingContext = WeighingContext.create(parameters, positionContext)
reference.resolveToSymbols()
.asSequence()
.mapNotNull { it.staticScope }
.flatMap { it.completeClassifiers(positionContext) }
.flatMap {
createClassifierLookupElement(
classifierSymbol = it,
expectedType = weighingContext.expectedType,
importingStrategy = importingStrategy,
positionContext = positionContext,
)
}.map { it.withPresentableText(selectorExpression.text + "." + it.lookupString) }
}
sink.registerChainContributor(this@FirClassifierCompletionContributor)
}
}
}
@@ -179,6 +158,31 @@ internal open class FirClassifierCompletionContributor(
indexClassifiers
}
context(KaSession)
override fun createChainedLookupElements(
positionContext: KotlinNameReferencePositionContext,
receiverExpression: KtDotQualifiedExpression,
importingStrategy: ImportStrategy
): Sequence<LookupElement> {
val selectorExpression = receiverExpression.selectorExpression ?: return emptySequence()
val reference = receiverExpression.reference() ?: return emptySequence()
val weighingContext = WeighingContext.create(parameters, positionContext)
return reference.resolveToSymbols()
.asSequence()
.mapNotNull { it.staticScope }
.flatMap { it.completeClassifiers(positionContext) }
.flatMap {
createClassifierLookupElement(
classifierSymbol = it,
expectedType = weighingContext.expectedType,
importingStrategy = importingStrategy,
positionContext = positionContext,
)
}.map { it.withPresentableText(selectorExpression.text + "." + it.lookupString) }
}
context(KaSession)
private fun createClassifierLookupElement(
@@ -8,9 +8,8 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.registry.RegistryManager
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.components.KaScopeKind
import org.jetbrains.kotlin.analysis.api.signatures.KaCallableSignature
import org.jetbrains.kotlin.analysis.api.signatures.KaFunctionSignature
@@ -32,19 +31,18 @@ import org.jetbrains.kotlin.idea.completion.impl.k2.ImportStrategyDetector
import org.jetbrains.kotlin.idea.completion.impl.k2.LookupElementSink
import org.jetbrains.kotlin.idea.completion.lookups.CallableInsertionOptions
import org.jetbrains.kotlin.idea.completion.lookups.ImportStrategy
import org.jetbrains.kotlin.idea.completion.lookups.factories.ClassifierLookupObject
import org.jetbrains.kotlin.idea.completion.lookups.factories.FunctionCallLookupObject
import org.jetbrains.kotlin.idea.completion.lookups.factories.FunctionLookupElementFactory
import org.jetbrains.kotlin.idea.completion.lookups.factories.KotlinFirLookupElementFactory
import org.jetbrains.kotlin.idea.completion.weighers.CallableWeigher.callableWeight
import org.jetbrains.kotlin.idea.completion.weighers.Weighers.applyWeighs
import org.jetbrains.kotlin.idea.completion.weighers.WeighingContext
import org.jetbrains.kotlin.idea.util.positionContext.KotlinExpressionNameReferencePositionContext
import org.jetbrains.kotlin.idea.util.positionContext.KotlinNameReferencePositionContext
import org.jetbrains.kotlin.idea.util.positionContext.KotlinRawPositionContext
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.util.OperatorNameConventions
internal abstract class FirCompletionContributorBase<C : KotlinRawPositionContext>(
@@ -180,54 +178,6 @@ internal abstract class FirCompletionContributorBase<C : KotlinRawPositionContex
}
}
protected fun runChainCompletion(
positionContext: KotlinNameReferencePositionContext,
explicitReceiver: KtElement,
createLookupElements: KaSession.(
receiverExpression: KtDotQualifiedExpression,
positionContext: KotlinExpressionNameReferencePositionContext,
importingStrategy: ImportStrategy.AddImport,
) -> Sequence<LookupElement>,
) {
if (!RegistryManager.getInstance().`is`("kotlin.k2.chain.completion.enabled")) return
sink.runRemainingContributors(parameters.delegate) { completionResult ->
val lookupElement = completionResult.lookupElement
val classifierLookupObject = lookupElement.`object` as? ClassifierLookupObject
val nameToImport = when (val importStrategy = classifierLookupObject?.importingStrategy) {
is ImportStrategy.AddImport -> importStrategy.nameToImport
is ImportStrategy.InsertFqNameAndShorten -> importStrategy.fqName
else -> null
}
if (nameToImport == null) {
sink.passResult(completionResult)
return@runRemainingContributors
}
val expression = KtPsiFactory.contextual(explicitReceiver)
.createExpression(nameToImport.render() + "." + positionContext.nameExpression.text) as KtDotQualifiedExpression
val receiverExpression = expression.receiverExpression as? KtDotQualifiedExpression
val nameExpression = expression.selectorExpression as? KtNameReferenceExpression
if (receiverExpression == null
|| nameExpression == null
) {
sink.passResult(completionResult)
return@runRemainingContributors
}
analyze(nameExpression) {
createLookupElements(
/* receiverExpression = */ receiverExpression,
/* positionContext = */ KotlinExpressionNameReferencePositionContext(nameExpression),
/* importingStrategy = */ ImportStrategy.AddImport(nameToImport),
).forEach(sink::addElement)
}
}
}
// todo move out
// todo move to the corresponding assignment
protected fun LookupElementBuilder.adaptToExplicitReceiver(
@@ -270,3 +220,14 @@ internal abstract class FirCompletionContributorBase<C : KotlinRawPositionContex
else -> this
}
}
@ApiStatus.Experimental // todo reconsider
internal interface ChainCompletionContributor : FirCompletionContributor<KotlinNameReferencePositionContext> {
context(KaSession)
fun createChainedLookupElements(
positionContext: KotlinNameReferencePositionContext,
receiverExpression: KtDotQualifiedExpression,
importingStrategy: ImportStrategy,
): Sequence<LookupElement>
}
@@ -0,0 +1,5 @@
package foo
object Foo {
val value: Int = 5
}
@@ -0,0 +1,9 @@
fun test() {
Foo.val<caret>
}
// EXIST: .val
// EXIST: value
// NUMBER: 2
// NOTHING_ELSE
@@ -50,6 +50,11 @@ public class MultiFileJvmBasicCompletionTestGenerated extends AbstractMultiFileJ
runTest("../testData/basic/multifile/CallablesInExcludedPackage/");
}
@TestMetadata("ChainCompletionDontDuplicate")
public void testChainCompletionDontDuplicate() throws Exception {
runTest("../testData/basic/multifile/ChainCompletionDontDuplicate/");
}
@TestMetadata("ClassInExcludedPackage")
public void testClassInExcludedPackage() throws Exception {
runTest("../testData/basic/multifile/ClassInExcludedPackage/");
@@ -50,6 +50,11 @@ public class HighLevelMultiFileJvmBasicCompletionTestGenerated extends AbstractH
runTest("../../completion/testData/basic/multifile/CallablesInExcludedPackage/");
}
@TestMetadata("ChainCompletionDontDuplicate")
public void testChainCompletionDontDuplicate() throws Exception {
runTest("../../completion/testData/basic/multifile/ChainCompletionDontDuplicate/");
}
@TestMetadata("ClassInExcludedPackage")
public void testClassInExcludedPackage() throws Exception {
runTest("../../completion/testData/basic/multifile/ClassInExcludedPackage/");