diff --git a/idea/idea-completion/testData/basic/multifile/CallableReferenceNotImported/CallableReferenceNotImported.kt b/idea/idea-completion/testData/basic/multifile/CallableReferenceNotImported/CallableReferenceNotImported.kt index 9853a70378f..b1507f8026f 100644 --- a/idea/idea-completion/testData/basic/multifile/CallableReferenceNotImported/CallableReferenceNotImported.kt +++ b/idea/idea-completion/testData/basic/multifile/CallableReferenceNotImported/CallableReferenceNotImported.kt @@ -1,4 +1,3 @@ -// FIR_COMPARISON fun String.foo() { val v = ::xxx_ } diff --git a/idea/idea-completion/testData/basic/multifile/DoNotCompleteWithConstraints/DoNotCompleteWithConstraints.kt b/idea/idea-completion/testData/basic/multifile/DoNotCompleteWithConstraints/DoNotCompleteWithConstraints.kt index 09f9c5343ef..9414c095e86 100644 --- a/idea/idea-completion/testData/basic/multifile/DoNotCompleteWithConstraints/DoNotCompleteWithConstraints.kt +++ b/idea/idea-completion/testData/basic/multifile/DoNotCompleteWithConstraints/DoNotCompleteWithConstraints.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first interface TestedTrait() { diff --git a/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.dependency.kt b/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.dependency.kt index ea97b9f31f3..798bcd2e5b5 100644 --- a/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.dependency.kt +++ b/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.dependency.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package second import first.FirstClass diff --git a/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.kt b/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.kt index b4a0c10b1e3..98523e7c83c 100644 --- a/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.kt +++ b/idea/idea-completion/testData/basic/multifile/ExtensionFunction/ExtensionFunction.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first class FirstClass() { diff --git a/idea/idea-completion/testData/basic/multifile/ExtensionFunctionOnImportedFunction/ExtensionFunctionOnImportedFunction.kt b/idea/idea-completion/testData/basic/multifile/ExtensionFunctionOnImportedFunction/ExtensionFunctionOnImportedFunction.kt index e2fea3e03f8..481b16a7d6e 100644 --- a/idea/idea-completion/testData/basic/multifile/ExtensionFunctionOnImportedFunction/ExtensionFunctionOnImportedFunction.kt +++ b/idea/idea-completion/testData/basic/multifile/ExtensionFunctionOnImportedFunction/ExtensionFunctionOnImportedFunction.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first // For KT-3102 diff --git a/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction2/NotImportedExtensionFunction2.kt b/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction2/NotImportedExtensionFunction2.kt index 0a0e5529e27..5b4fbccbb87 100644 --- a/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction2/NotImportedExtensionFunction2.kt +++ b/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction2/NotImportedExtensionFunction2.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first fun firstFun(p: () -> Unit) { diff --git a/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction3/NotImportedExtensionFunction3.kt b/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction3/NotImportedExtensionFunction3.kt index dd2478a12c6..770b90091fb 100644 --- a/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction3/NotImportedExtensionFunction3.kt +++ b/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunction3/NotImportedExtensionFunction3.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first fun firstFun(p: String.() -> Unit) { diff --git a/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunctionAndAlias/NotImportedExtensionFunctionAndAlias.kt b/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunctionAndAlias/NotImportedExtensionFunctionAndAlias.kt index 6c40c2e074b..a5706c02db7 100644 --- a/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunctionAndAlias/NotImportedExtensionFunctionAndAlias.kt +++ b/idea/idea-completion/testData/basic/multifile/NotImportedExtensionFunctionAndAlias/NotImportedExtensionFunctionAndAlias.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first fun firstFun(x: third.Dependency) { diff --git a/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport-1.kt b/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport-1.kt index 9b743c13c61..9ebd3472871 100644 --- a/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport-1.kt +++ b/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport-1.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first fun firstFun() { diff --git a/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport.kt.after b/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport.kt.after index d2679999b61..5d505988772 100644 --- a/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport.kt.after +++ b/idea/idea-completion/testData/handlers/multifile/ExtensionFunctionImport.kt.after @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first import second.helloFun diff --git a/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport-1.kt b/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport-1.kt index 89133cde43b..71a2c44193c 100644 --- a/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport-1.kt +++ b/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport-1.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first fun foo() { diff --git a/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport.kt.after b/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport.kt.after index a2b7cea67b6..cb99e91f410 100644 --- a/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport.kt.after +++ b/idea/idea-completion/testData/handlers/multifile/ExtensionPropertyImport.kt.after @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first import second.extensionProp diff --git a/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName-1.kt b/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName-1.kt index dd3b6800c0d..b5255ed1fb1 100644 --- a/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName-1.kt +++ b/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName-1.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first fun foo() { diff --git a/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName.kt.after b/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName.kt.after index 7ed10ce523d..72ab7471a53 100644 --- a/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName.kt.after +++ b/idea/idea-completion/testData/handlers/multifile/KeywordExtensionFunctionName.kt.after @@ -1,3 +1,4 @@ +// FIR_COMPARISON package first import second.`package` 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 83129670e14..76c72e38472 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 @@ -16,14 +16,17 @@ 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.InvalidWayOfUsingAnalysisSession import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.components.KtScopeContext import org.jetbrains.kotlin.idea.frontend.api.getAnalysisSessionFor import org.jetbrains.kotlin.idea.frontend.api.scopes.KtCompositeScope import org.jetbrains.kotlin.idea.frontend.api.scopes.KtScope import org.jetbrains.kotlin.idea.frontend.api.scopes.KtScopeNameFilter import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassOrObjectSymbol import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSymbol import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtNamedSymbol import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.isExtension +import org.jetbrains.kotlin.idea.frontend.api.types.KtClassType import org.jetbrains.kotlin.idea.frontend.api.types.KtType import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.lexer.KtTokens @@ -89,6 +92,7 @@ private class KotlinCommonCompletionProvider( private val indexHelper: IndexHelper ) { private val lookupElementFactory = KotlinFirLookupElementFactory() + private val typeNamesProvider = TypeNamesProvider(indexHelper) private val scopeNameFilter: KtScopeNameFilter = { name -> !name.isSpecial && prefixMatcher.prefixMatches(name.identifier) } @@ -132,21 +136,21 @@ private class KotlinCommonCompletionProvider( with(getAnalysisSessionFor(originalFile).createContextDependentCopy(originalFile, nameExpression)) { val expectedType = nameExpression.getExpectedType() - val (implicitScopes, _) = originalFile.getScopeContextForPosition(nameExpression) + val scopesContext = originalFile.getScopeContextForPosition(nameExpression) fun KtCallableSymbol.hasSuitableExtensionReceiver(): Boolean = checkExtensionIsSuitable(originalFile, nameExpression, explicitReceiver) when { - nameExpression.parent is KtUserType -> collectTypesCompletion(result, implicitScopes, expectedType) + nameExpression.parent is KtUserType -> collectTypesCompletion(result, scopesContext.scopes, expectedType) explicitReceiver != null -> collectDotCompletion( result, - implicitScopes, + scopesContext.scopes, explicitReceiver, expectedType, KtCallableSymbol::hasSuitableExtensionReceiver ) - else -> collectDefaultCompletion(result, implicitScopes, expectedType, KtCallableSymbol::hasSuitableExtensionReceiver) + else -> collectDefaultCompletion(result, scopesContext, expectedType, KtCallableSymbol::hasSuitableExtensionReceiver) } } } @@ -183,14 +187,20 @@ private class KotlinCommonCompletionProvider( nonExtensionMembers.forEach { addSymbolToCompletion(result, expectedType, it) } extensionNonMembers.forEach { addSymbolToCompletion(result, expectedType, it) } + + collectTopLevelExtensionsFromIndices(listOf(typeOfPossibleReceiver), hasSuitableExtensionReceiver) + .forEach { addSymbolToCompletion(result, expectedType, it) } + } private fun KtAnalysisSession.collectDefaultCompletion( result: CompletionResultSet, - implicitScopes: KtCompositeScope, + implicitScopesContext: KtScopeContext, expectedType: KtType?, hasSuitableExtensionReceiver: KtCallableSymbol.() -> Boolean, ) { + val (implicitScopes, implicitReceiversTypes) = implicitScopesContext + val availableNonExtensions = implicitScopes .getCallableSymbols(scopeNameFilter) .filterNot { it.isExtension } @@ -209,7 +219,43 @@ private class KotlinCommonCompletionProvider( .forEach { addSymbolToCompletion(result, expectedType, it) } } + collectTopLevelExtensionsFromIndices(implicitReceiversTypes, hasSuitableExtensionReceiver) + .forEach { addSymbolToCompletion(result, expectedType, it) } collectTypesCompletion(result, implicitScopes, expectedType) } + + private fun KtAnalysisSession.collectTopLevelExtensionsFromIndices( + receiverTypes: List, + hasSuitableExtensionReceiver: KtCallableSymbol.() -> Boolean + ): Sequence { + val implicitReceiverNames = findAllNamesOfTypes(receiverTypes) + val topLevelExtensions = indexHelper.getTopLevelExtensions(scopeNameFilter, implicitReceiverNames) + + return topLevelExtensions.asSequence() + .map { it.getSymbol() as KtCallableSymbol } + .filter(hasSuitableExtensionReceiver) + } + + private fun KtAnalysisSession.findAllNamesOfTypes(implicitReceiversTypes: List) = + implicitReceiversTypes.flatMapTo(hashSetOf()) { with(typeNamesProvider) { findAllNames(it) } } +} + +private class TypeNamesProvider(private val indexHelper: IndexHelper) { + fun KtAnalysisSession.findAllNames(type: KtType): Set { + if (type !is KtClassType) return emptySet() + + val result = hashSetOf() + val typeName = type.classId.shortClassName.identifier + + result += typeName + result += indexHelper.getPossibleTypeAliasExpansionNames(typeName) + + val superTypes = (type.classSymbol as? KtClassOrObjectSymbol)?.superTypes + superTypes?.forEach { superType -> + result += findAllNames(superType.type) + } + + return result + } } \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirLookupElementFactory.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirLookupElementFactory.kt index 60bc17f0195..1f212cc65e6 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirLookupElementFactory.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirLookupElementFactory.kt @@ -20,6 +20,7 @@ import org.jetbrains.kotlin.idea.core.withRootPrefixIfNeeded import org.jetbrains.kotlin.idea.frontend.api.HackToForceAllowRunningAnalyzeOnEDT import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession import org.jetbrains.kotlin.idea.frontend.api.analyze +import org.jetbrains.kotlin.idea.frontend.api.fir.utils.addImportToFile import org.jetbrains.kotlin.idea.frontend.api.hackyAllowRunningOnEdt import org.jetbrains.kotlin.idea.frontend.api.symbols.* import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtNamedSymbol @@ -115,10 +116,13 @@ private class VariableLookupElementFactory { if (setterName != null) "$getterName()/$setterName()" else "$getterName()" private fun createInsertHandler(symbol: KtVariableLikeSymbol): InsertHandler { - val callableId = symbol.callableIdIfExists - if (callableId == null || !symbol.canBeCalledByFqName) return QuotedNamesAwareInsertionHandler(symbol.name) + val callableId = symbol.callableIdIfExists ?: return QuotedNamesAwareInsertionHandler(symbol.name) - return ShorteningVariableInsertionHandler(callableId) + return if (symbol.canBeCalledByFqName) { + ShorteningVariableInsertionHandler(callableId) + } else { + SimpleVariableInsertionHandler(callableId) + } } private val KtVariableLikeSymbol.callableIdIfExists: FqName? @@ -181,7 +185,6 @@ private class FunctionLookupElementFactory { private fun KtAnalysisSession.createInsertHandler(symbol: KtFunctionSymbol): InsertHandler { val functionFqName = symbol.callableIdIfNonLocal - return if (functionFqName != null && canBeCalledByFqName(symbol)) { ShorteningFunctionInsertionHandler( functionFqName, @@ -192,11 +195,15 @@ private class FunctionLookupElementFactory { SimpleFunctionInsertionHandler( symbol.name, inputValueArguments = symbol.valueParameters.isNotEmpty(), - insertEmptyLambda = insertLambdaBraces(symbol) + insertEmptyLambda = insertLambdaBraces(symbol), + nameToImport = symbol.importableFqName, ) } } + private val KtFunctionSymbol.importableFqName: FqName? + get() = if (dispatchType == null) callableIdIfNonLocal else null + private fun canBeCalledByFqName(symbol: KtFunctionSymbol): Boolean { return !symbol.isExtension && symbol.dispatchType == null } @@ -299,15 +306,22 @@ private abstract class AbstractFunctionInsertionHandler( private class SimpleFunctionInsertionHandler( name: Name, inputValueArguments: Boolean, - insertEmptyLambda: Boolean + insertEmptyLambda: Boolean, + private val nameToImport: FqName? ) : AbstractFunctionInsertionHandler(name, inputValueArguments, insertEmptyLambda) { override fun handleInsert(context: InsertionContext, item: LookupElement) { super.handleInsert(context, item) + val targetFile = context.file as? KtFile ?: return + val startOffset = context.startOffset val element = context.file.findElementAt(startOffset) ?: return addArguments(context, element) + + if (nameToImport != null && targetFile.importDirectives.none { it.importPath?.fqName == nameToImport }) { + addImportToFile(context.project, targetFile, nameToImport) + } } } @@ -352,6 +366,19 @@ private class ShorteningVariableInsertionHandler(private val name: FqName) : Ins } } +private class SimpleVariableInsertionHandler(private val nameToImport: FqName) : + QuotedNamesAwareInsertionHandler(nameToImport.shortName()) { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + super.handleInsert(context, item) + + val targetFile = context.file as? KtFile ?: return + + if (targetFile.importDirectives.none { it.importPath?.fqName == nameToImport }) { + addImportToFile(context.project, targetFile, nameToImport) + } + } +} + private open class QuotedNamesAwareInsertionHandler(private val name: Name) : InsertHandler { override fun handleInsert(context: InsertionContext, item: LookupElement) { val startOffset = context.startOffset diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt index 4f70196afe2..022453d3bf5 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt @@ -98,6 +98,33 @@ public class IndexHelper(val project: Project, private val scope: GlobalSearchSc return (functions + properties).toList() } + fun getTopLevelExtensions(nameFilter: (Name) -> Boolean, receiverTypeNames: Set): Collection { + val index = KotlinTopLevelExtensionsByReceiverTypeIndex.INSTANCE + + return index.getAllKeys(project).asSequence() + .onEach { ProgressManager.checkCanceled() } + .filter { KotlinTopLevelExtensionsByReceiverTypeIndex.receiverTypeNameFromKey(it) in receiverTypeNames } + .filter { nameFilter(Name.identifier(KotlinTopLevelExtensionsByReceiverTypeIndex.callableNameFromKey(it))) } + .flatMap { key -> index[key, project, scope] } + .toList() + } + + fun getPossibleTypeAliasExpansionNames(originalTypeName: String): Set { + val index = KotlinTypeAliasByExpansionShortNameIndex.INSTANCE + val out = mutableSetOf() + + fun searchRecursively(typeName: String) { + ProgressManager.checkCanceled() + index[typeName, project, scope].asSequence() + .mapNotNull { it.name } + .filter { out.add(it) } + .forEach(::searchRecursively) + } + + searchRecursively(originalTypeName) + return out + } + companion object { private fun CallableId.asStringForIndexes(): String = (if (packageName.isRoot) callableName.asString() else toString()).replace('/', '.') diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/shortenReferencesUtils.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/shortenReferencesUtils.kt index 00d6a8af987..927519b5aae 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/shortenReferencesUtils.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/shortenReferencesUtils.kt @@ -21,7 +21,7 @@ private val SimpleImportPathComparator: Comparator = compareBy(Impor * * We want it as a copy because we do not yet care about imports ordering, so we do not need a fancy comparator. */ -internal fun addImportToFile( +fun addImportToFile( project: Project, file: KtFile, fqName: FqName,