diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/Completions.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/Completions.kt index 677a5e71725c..a794c5f85d35 100644 --- a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/Completions.kt +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/Completions.kt @@ -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() // 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, + ) { + 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 { diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/LookupElementSink.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/LookupElementSink.kt index 0adfd323a1c6..2461c4e0b941 100644 --- a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/LookupElementSink.kt +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/LookupElementSink.kt @@ -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>? = 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>): LookupElementSink = - LookupElementSink(resultSet, parameters, groupPriority, contributorClass) + LookupElementSink(resultSet, parameters, groupPriority, contributorClass, registerChainContributor) fun passResult(result: CompletionResult) { resultSet.passResult(result) diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCallableCompletionContributor.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCallableCompletionContributor.kt index 6d693eaad128..ec4609a4997e 100644 --- a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCallableCompletionContributor.kt +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCallableCompletionContributor.kt @@ -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(sink, priority) { +) : FirCompletionContributorBase(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 { + 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) } } } diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirClassifierCompletionContributor.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirClassifierCompletionContributor.kt index dfb988d5f2ec..8ab7bd2ad14a 100644 --- a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirClassifierCompletionContributor.kt +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirClassifierCompletionContributor.kt @@ -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(sink, priority) { +) : FirCompletionContributorBase(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 { + 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( diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCompletionContributorBase.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCompletionContributorBase.kt index 1658ca1c76d6..10b5bb674eac 100644 --- a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCompletionContributorBase.kt +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/FirCompletionContributorBase.kt @@ -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( @@ -180,54 +178,6 @@ internal abstract class FirCompletionContributorBase Sequence, - ) { - 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 this } } + +@ApiStatus.Experimental // todo reconsider +internal interface ChainCompletionContributor : FirCompletionContributor { + + context(KaSession) + fun createChainedLookupElements( + positionContext: KotlinNameReferencePositionContext, + receiverExpression: KtDotQualifiedExpression, + importingStrategy: ImportStrategy, + ): Sequence +} \ No newline at end of file diff --git a/plugins/kotlin/completion/testData/basic/multifile/ChainCompletionDontDuplicate/ChainCompletionDontDuplicate.dependency.kt b/plugins/kotlin/completion/testData/basic/multifile/ChainCompletionDontDuplicate/ChainCompletionDontDuplicate.dependency.kt new file mode 100644 index 000000000000..161519e549b2 --- /dev/null +++ b/plugins/kotlin/completion/testData/basic/multifile/ChainCompletionDontDuplicate/ChainCompletionDontDuplicate.dependency.kt @@ -0,0 +1,5 @@ +package foo + +object Foo { + val value: Int = 5 +} \ No newline at end of file diff --git a/plugins/kotlin/completion/testData/basic/multifile/ChainCompletionDontDuplicate/ChainCompletionDontDuplicate.kt b/plugins/kotlin/completion/testData/basic/multifile/ChainCompletionDontDuplicate/ChainCompletionDontDuplicate.kt new file mode 100644 index 000000000000..d7e01a99453c --- /dev/null +++ b/plugins/kotlin/completion/testData/basic/multifile/ChainCompletionDontDuplicate/ChainCompletionDontDuplicate.kt @@ -0,0 +1,9 @@ + +fun test() { + Foo.val +} + +// EXIST: .val +// EXIST: value +// NUMBER: 2 +// NOTHING_ELSE \ No newline at end of file diff --git a/plugins/kotlin/completion/tests-k1/test/org/jetbrains/kotlin/idea/completion/test/MultiFileJvmBasicCompletionTestGenerated.java b/plugins/kotlin/completion/tests-k1/test/org/jetbrains/kotlin/idea/completion/test/MultiFileJvmBasicCompletionTestGenerated.java index 260e1e5c9e1b..fa1df5a5d3fb 100644 --- a/plugins/kotlin/completion/tests-k1/test/org/jetbrains/kotlin/idea/completion/test/MultiFileJvmBasicCompletionTestGenerated.java +++ b/plugins/kotlin/completion/tests-k1/test/org/jetbrains/kotlin/idea/completion/test/MultiFileJvmBasicCompletionTestGenerated.java @@ -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/"); diff --git a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/HighLevelMultiFileJvmBasicCompletionTestGenerated.java b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/HighLevelMultiFileJvmBasicCompletionTestGenerated.java index 6123a641baf5..ba26bd82dc66 100644 --- a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/HighLevelMultiFileJvmBasicCompletionTestGenerated.java +++ b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/HighLevelMultiFileJvmBasicCompletionTestGenerated.java @@ -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/");