From aef071691fe792795bb5d6f4633539127bee7e99 Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Wed, 26 May 2021 17:00:56 +0200 Subject: [PATCH] FIR IDE: refactor: separate lookup element factories into different files --- .../KotlinFirLookupElementFactory.kt | 486 ------------------ .../context/FirBasicCompletionContext.kt | 2 +- .../FirCompletionContributorBase.kt | 6 +- ...tivePackageMembersCompletionContributor.kt | 4 +- .../helpers/FirSuperEntriesProvider.kt | 5 +- .../lookups/CallableImportStrategy.kt | 57 ++ .../lookups/CallableInsertionStrategy.kt | 11 + .../lookups/CompletionShortNamesRenderer.kt | 21 + .../completion/lookups/KotlinLookupObject.kt | 12 + .../QuotedNamesAwareInsertionHandler.kt | 26 + .../completion/lookups/UniqueLookupObject.kt | 11 + .../factories/ClassLookupElementFactory.kt | 57 ++ .../factories/FunctionLookupElementFactory.kt | 206 ++++++++ .../KotlinFirLookupElementFactory.kt | 59 +++ .../PackagePartLookupElementFactory.kt | 47 ++ .../TypeParameterLookupElementFactory.kt | 19 + .../factories/VariableLookupElementFactory.kt | 93 ++++ .../kotlin/idea/completion/lookups/utils.kt | 52 ++ 18 files changed, 678 insertions(+), 496 deletions(-) create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableImportStrategy.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableInsertionStrategy.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CompletionShortNamesRenderer.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/KotlinLookupObject.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/QuotedNamesAwareInsertionHandler.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/UniqueLookupObject.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/ClassLookupElementFactory.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/FunctionLookupElementFactory.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/KotlinFirLookupElementFactory.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/PackagePartLookupElementFactory.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/TypeParameterLookupElementFactory.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/VariableLookupElementFactory.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/utils.kt 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 fdac62b9600..e69de29bb2d 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 @@ -1,486 +0,0 @@ -/* - * Copyright 2010-2020 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.AutoPopupController -import com.intellij.codeInsight.completion.InsertHandler -import com.intellij.codeInsight.completion.InsertionContext -import com.intellij.codeInsight.lookup.Lookup -import com.intellij.codeInsight.lookup.LookupElement -import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.icons.AllIcons -import com.intellij.openapi.diagnostic.ControlFlowException -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import org.jetbrains.kotlin.idea.completion.contributors.helpers.addDotAndInvokeCompletion -import org.jetbrains.kotlin.idea.completion.handlers.isTextAt -import org.jetbrains.kotlin.idea.core.asFqNameWithRootPrefixIfNeeded -import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession -import org.jetbrains.kotlin.idea.frontend.api.analyse -import org.jetbrains.kotlin.idea.frontend.api.components.KtTypeRendererOptions -import org.jetbrains.kotlin.idea.frontend.api.fir.utils.addImportToFile -import org.jetbrains.kotlin.idea.frontend.api.symbols.* -import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtNamedSymbol -import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtPossibleMemberSymbol -import org.jetbrains.kotlin.idea.frontend.api.tokens.HackToForceAllowRunningAnalyzeOnEDT -import org.jetbrains.kotlin.idea.frontend.api.tokens.hackyAllowRunningOnEdt -import org.jetbrains.kotlin.idea.frontend.api.types.KtFunctionalType -import org.jetbrains.kotlin.lexer.KtTokens -import org.jetbrains.kotlin.miniStdLib.letIf -import org.jetbrains.kotlin.name.* -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtTypeArgumentList -import org.jetbrains.kotlin.psi.psiUtil.endOffset -import org.jetbrains.kotlin.renderer.render - -internal class KotlinFirLookupElementFactory { - private val classLookupElementFactory = ClassLookupElementFactory() - private val variableLookupElementFactory = VariableLookupElementFactory() - private val functionLookupElementFactory = FunctionLookupElementFactory() - private val typeParameterLookupElementFactory = TypeParameterLookupElementFactory() - - fun KtAnalysisSession.createLookupElement(symbol: KtNamedSymbol): LookupElement? { - return when (symbol) { - is KtCallableSymbol -> createCallableLookupElement(symbol, importingStrategy = detectImportStrategy(symbol), CallableInsertionStrategy.AS_CALL) - is KtClassLikeSymbol -> with(classLookupElementFactory) { createLookup(symbol) } - is KtTypeParameterSymbol -> with(typeParameterLookupElementFactory) { createLookup(symbol) } - else -> throw IllegalArgumentException("Cannot create a lookup element for $symbol") - } - } - - fun KtAnalysisSession.createCallableLookupElement( - symbol: KtCallableSymbol, - importingStrategy: CallableImportStrategy, - insertionStrategy: CallableInsertionStrategy, - ): LookupElementBuilder? { - return when (symbol) { - is KtFunctionSymbol -> with(functionLookupElementFactory) { createLookup(symbol, importingStrategy, insertionStrategy) } - is KtVariableLikeSymbol -> with(variableLookupElementFactory) { createLookup(symbol, importingStrategy) } - else -> throw IllegalArgumentException("Cannot create a lookup element for $symbol") - } - } - - fun createPackagePartLookupElement(packagePartFqName: FqName): LookupElement { - val shortName = packagePartFqName.shortName() - return LookupElementBuilder.create(PackagePartLookupObject(shortName), "${shortName.render()}.") - .withInsertHandler(PackagePartInsertionHandler) - .withIcon(AllIcons.Nodes.Package) - .letIf(!packagePartFqName.parent().isRoot) { - it.appendTailText("(${packagePartFqName.asString()})", true) - } - } - - fun KtAnalysisSession.createLookupElementForClassLikeSymbol(symbol: KtClassLikeSymbol, insertFqName: Boolean = true): LookupElement? { - if (symbol !is KtNamedSymbol) return null - return with(classLookupElementFactory) { createLookup(symbol, insertFqName) } - } -} - -private fun KtAnalysisSession.withSymbolInfo( - symbol: KtSymbol, - elementBuilder: LookupElementBuilder -): LookupElementBuilder = elementBuilder - .withPsiElement(symbol.psi) // TODO check if it is a heavy operation and should be postponed - .withIcon(KotlinFirIconProvider.getIconFor(symbol)) - -internal sealed class CallableImportStrategy { - object DoNothing : CallableImportStrategy() - data class AddImport(val nameToImport: CallableId) : CallableImportStrategy() - data class InsertFqNameAndShorten(val callableId: CallableId) : CallableImportStrategy() -} - -/** - * This is a temporary hack to prevent clash of the lookup elements with same names. - */ -private class UniqueLookupObject - -private interface KotlinLookupObject { - val shortName: Name -} - -private data class PackagePartLookupObject( - override val shortName: Name, -) : KotlinLookupObject - -private data class ClassifierLookupObject(override val shortName: Name, val classId: ClassId?, val insertFqName: Boolean) : - KotlinLookupObject - -/** - * Simplest lookup object so two lookup elements for the same function will clash. - */ -private data class FunctionLookupObject( - override val shortName: Name, - val importStrategy: CallableImportStrategy, - val inputValueArguments: Boolean, - val insertEmptyLambda: Boolean, - // for distinction between different overloads - private val renderedFunctionParameters: String -) : KotlinLookupObject - -/** - * Simplest lookup object so two lookup elements for the same property will clash. - */ -private data class VariableLookupObject( - override val shortName: Name, - val importStrategy: CallableImportStrategy -) : KotlinLookupObject - -class ClassLookupElementFactory { - fun KtAnalysisSession.createLookup(symbol: KtClassLikeSymbol, insertFqName: Boolean = true): LookupElementBuilder { - val name = symbol.nameOrAnonymous - return LookupElementBuilder.create(ClassifierLookupObject(name, symbol.classIdIfNonLocal, insertFqName), name.asString()) - .withInsertHandler(ClassifierInsertionHandler) - .let { withSymbolInfo(symbol, it) } - } -} - -private class TypeParameterLookupElementFactory { - fun KtAnalysisSession.createLookup(symbol: KtTypeParameterSymbol): LookupElementBuilder { - return LookupElementBuilder - .create(UniqueLookupObject(), symbol.name.asString()) - .let { withSymbolInfo(symbol, it) } - } -} - -private class VariableLookupElementFactory { - fun KtAnalysisSession.createLookup( - symbol: KtVariableLikeSymbol, - importStrategy: CallableImportStrategy = detectImportStrategy(symbol) - ): LookupElementBuilder { - val lookupObject = VariableLookupObject( - symbol.name, - importStrategy = importStrategy - ) - - return LookupElementBuilder.create(lookupObject, symbol.name.asString()) - .withTypeText(symbol.annotatedType.type.render(WITH_TYPE_RENDERING_OPTIONS)) - .markIfSyntheticJavaProperty(symbol) - .withInsertHandler(VariableInsertionHandler) - .let { withSymbolInfo(symbol, it) } - } - - private fun LookupElementBuilder.markIfSyntheticJavaProperty(symbol: KtVariableLikeSymbol): LookupElementBuilder = when (symbol) { - is KtSyntheticJavaPropertySymbol -> { - val getterName = symbol.javaGetterName.asString() - val setterName = symbol.javaSetterName?.asString() - this.withTailText((" (from ${buildSyntheticPropertyTailText(getterName, setterName)})")) - .withLookupStrings(listOfNotNull(getterName, setterName)) - } - else -> this - } - - private fun buildSyntheticPropertyTailText(getterName: String, setterName: String?): String = - if (setterName != null) "$getterName()/$setterName()" else "$getterName()" -} - -private fun detectImportStrategy(symbol: KtCallableSymbol): CallableImportStrategy { - if (symbol !is KtPossibleMemberSymbol || symbol.dispatchType != null) return CallableImportStrategy.DoNothing - - val propertyId = symbol.callableIdIfNonLocal ?: return CallableImportStrategy.DoNothing - - return if (symbol.isExtension) { - CallableImportStrategy.AddImport(propertyId) - } else { - CallableImportStrategy.InsertFqNameAndShorten(propertyId) - } -} - -internal enum class CallableInsertionStrategy { - AS_CALL, - AS_IDENTIFIER -} - -private class FunctionLookupElementFactory { - fun KtAnalysisSession.createLookup( - symbol: KtFunctionSymbol, - importStrategy: CallableImportStrategy, - insertionStrategy: CallableInsertionStrategy - ): LookupElementBuilder? { - val lookupObject = FunctionLookupObject( - symbol.name, - importStrategy = importStrategy, - inputValueArguments = symbol.valueParameters.isNotEmpty(), - insertEmptyLambda = insertLambdaBraces(symbol), - renderedFunctionParameters = with(ShortNamesRenderer) { renderFunctionParameters(symbol) } - ) - - val insertionHandler = when (insertionStrategy) { - CallableInsertionStrategy.AS_CALL -> FunctionInsertionHandler - CallableInsertionStrategy.AS_IDENTIFIER -> QuotedNamesAwareInsertionHandler() - } - - return try { - LookupElementBuilder.create(lookupObject, symbol.name.asString()) - .withTailText(getTailText(symbol), true) - .withTypeText(symbol.annotatedType.type.render(WITH_TYPE_RENDERING_OPTIONS)) - .withInsertHandler(insertionHandler) - .let { withSymbolInfo(symbol, it) } - } catch (e: Throwable) { - if (e is ControlFlowException) throw e - LOG.error(e) - null - } - } - - private fun KtAnalysisSession.getTailText(symbol: KtFunctionSymbol): String { - return if (insertLambdaBraces(symbol)) " {...}" else with(ShortNamesRenderer) { renderFunctionParameters(symbol) } - } - - private fun KtAnalysisSession.insertLambdaBraces(symbol: KtFunctionSymbol): Boolean { - val singleParam = symbol.valueParameters.singleOrNull() - return singleParam != null && !singleParam.hasDefaultValue && singleParam.annotatedType.type is KtFunctionalType - } - - companion object { - private val LOG = logger() - } -} - -private val WITH_TYPE_RENDERING_OPTIONS = KtTypeRendererOptions.SHORT_NAMES - -/** - * The simplest implementation of the insertion handler for a classifiers. - */ -private object ClassifierInsertionHandler : InsertHandler { - override fun handleInsert(context: InsertionContext, item: LookupElement) { - val targetFile = context.file as? KtFile ?: return - val lookupObject = item.`object` as ClassifierLookupObject - - if (lookupObject.classId != null && lookupObject.insertFqName) { - val fqName = lookupObject.classId.asSingleFqName() - - context.document.replaceString(context.startOffset, context.tailOffset, fqName.render()) - context.commitDocument() - - shortenReferencesForFirCompletion(targetFile, TextRange(context.startOffset, context.tailOffset)) - } - } -} - -private object FunctionInsertionHandler : QuotedNamesAwareInsertionHandler() { - private fun addArguments(context: InsertionContext, offsetElement: PsiElement, lookupObject: FunctionLookupObject) { - val completionChar = context.completionChar - if (completionChar == '(') { //TODO: more correct behavior related to braces type - context.setAddCompletionChar(false) - } - - var offset = context.tailOffset - val document = context.document - val editor = context.editor - val project = context.project - val chars = document.charsSequence - - val isSmartEnterCompletion = completionChar == Lookup.COMPLETE_STATEMENT_SELECT_CHAR - val isReplaceCompletion = completionChar == Lookup.REPLACE_SELECT_CHAR - - val (openingBracket, closingBracket) = if (lookupObject.insertEmptyLambda) '{' to '}' else '(' to ')' - - if (isReplaceCompletion) { - val offset1 = chars.skipSpaces(offset) - if (offset1 < chars.length) { - if (chars[offset1] == '<') { - val token = context.file.findElementAt(offset1)!! - if (token.node.elementType == KtTokens.LT) { - val parent = token.parent - /* if type argument list is on multiple lines this is more likely wrong parsing*/ - if (parent is KtTypeArgumentList && parent.getText().indexOf('\n') < 0) { - offset = parent.endOffset - } - } - } - } - } - - var openingBracketOffset = chars.indexOfSkippingSpace(openingBracket, offset) - var closeBracketOffset = openingBracketOffset?.let { chars.indexOfSkippingSpace(closingBracket, it + 1) } - var inBracketsShift = 0 - - if (openingBracketOffset == null) { - if (lookupObject.insertEmptyLambda) { - if (completionChar == ' ' || completionChar == '{') { - context.setAddCompletionChar(false) - } - - if (isInsertSpacesInOneLineFunctionEnabled(context.file)) { - document.insertString(offset, " { }") - inBracketsShift = 1 - } else { - document.insertString(offset, " {}") - } - } else { - if (isSmartEnterCompletion) { - document.insertString(offset, "(") - } else { - document.insertString(offset, "()") - } - } - context.commitDocument() - - openingBracketOffset = document.charsSequence.indexOfSkippingSpace(openingBracket, offset)!! - closeBracketOffset = document.charsSequence.indexOfSkippingSpace(closingBracket, openingBracketOffset + 1) - } - - if (shouldPlaceCaretInBrackets(completionChar, lookupObject) || closeBracketOffset == null) { - editor.caretModel.moveToOffset(openingBracketOffset + 1 + inBracketsShift) - AutoPopupController.getInstance(project)?.autoPopupParameterInfo(editor, offsetElement) - } else { - editor.caretModel.moveToOffset(closeBracketOffset + 1) - } - } - - private fun shouldPlaceCaretInBrackets(completionChar: Char, lookupObject: FunctionLookupObject): Boolean { - if (completionChar == ',' || completionChar == '.' || completionChar == '=') return false - if (completionChar == '(') return true - return lookupObject.inputValueArguments || lookupObject.insertEmptyLambda - } - - // FIXME Should be fetched from language settings (INSERT_WHITESPACES_IN_SIMPLE_ONE_LINE_METHOD), but we do not have them right now - private fun isInsertSpacesInOneLineFunctionEnabled(file: PsiElement) = true - - override fun handleInsert(context: InsertionContext, item: LookupElement) { - val targetFile = context.file as? KtFile ?: return - val lookupObject = item.`object` as FunctionLookupObject - - super.handleInsert(context, item) - - val startOffset = context.startOffset - val element = context.file.findElementAt(startOffset) ?: return - - val importStrategy = lookupObject.importStrategy - if (importStrategy is CallableImportStrategy.InsertFqNameAndShorten) { - context.document.replaceString( - context.startOffset, - context.tailOffset, - importStrategy.callableId.asFqNameWithRootPrefixIfNeeded().render() - ) - context.commitDocument() - - addArguments(context, element, lookupObject) - context.commitDocument() - - shortenReferencesForFirCompletion(targetFile, TextRange(context.startOffset, context.tailOffset)) - } else { - addArguments(context, element, lookupObject) - context.commitDocument() - - if (importStrategy is CallableImportStrategy.AddImport) { - addCallableImportIfRequired(targetFile, importStrategy.nameToImport) - } - } - } -} - -private object VariableInsertionHandler : InsertHandler { - override fun handleInsert(context: InsertionContext, item: LookupElement) { - val targetFile = context.file as? KtFile ?: return - val lookupObject = item.`object` as VariableLookupObject - - when (val importStrategy = lookupObject.importStrategy) { - is CallableImportStrategy.AddImport -> { - addCallableImportIfRequired(targetFile, importStrategy.nameToImport) - } - - is CallableImportStrategy.InsertFqNameAndShorten -> { - context.document.replaceString( - context.startOffset, - context.tailOffset, - importStrategy.callableId.asFqNameWithRootPrefixIfNeeded().render() - ) - - context.commitDocument() - shortenReferencesForFirCompletion(targetFile, TextRange(context.startOffset, context.tailOffset)) - } - - is CallableImportStrategy.DoNothing -> { - } - } - } -} - -private object PackagePartInsertionHandler : InsertHandler { - override fun handleInsert(context: InsertionContext, item: LookupElement) { - val lookupElement = item.`object` as PackagePartLookupObject - val name = lookupElement.shortName.render() - context.document.replaceString(context.startOffset, context.tailOffset, name) - context.commitDocument() - context.addDotAndInvokeCompletion() - } -} - -private open class QuotedNamesAwareInsertionHandler : InsertHandler { - override fun handleInsert(context: InsertionContext, item: LookupElement) { - val lookupElement = item.`object` as KotlinLookupObject - - val startOffset = context.startOffset - if (startOffset > 0 && context.document.isTextAt(startOffset - 1, "`")) { - context.document.deleteString(startOffset - 1, startOffset) - } - context.document.replaceString(context.startOffset, context.tailOffset, lookupElement.shortName.render()) - - context.commitDocument() - } -} - -private fun addCallableImportIfRequired(targetFile: KtFile, nameToImport: CallableId) { - if (!alreadyHasImport(targetFile, nameToImport)) { - addImportToFile(targetFile.project, targetFile, nameToImport) - } -} - -private fun alreadyHasImport(file: KtFile, nameToImport: CallableId): Boolean { - if (file.importDirectives.any { it.importPath?.fqName == nameToImport.asSingleFqName() }) return true - - withAllowedResolve { - analyse(file) { - val scopes = file.getScopeContextForFile().scopes - if (!scopes.mayContainName(nameToImport.callableName)) return false - - return scopes - .getCallableSymbols { it == nameToImport.callableName } - .any { - it is KtKotlinPropertySymbol && it.callableIdIfNonLocal == nameToImport || - it is KtFunctionSymbol && it.callableIdIfNonLocal == nameToImport - } - } - } -} - -private object ShortNamesRenderer { - fun KtAnalysisSession.renderFunctionParameters(function: KtFunctionSymbol): String = - function.valueParameters.joinToString(", ", "(", ")") { renderFunctionParameter(it) } - - private fun KtAnalysisSession.renderFunctionParameter(param: KtValueParameterSymbol): String = - "${if (param.isVararg) "vararg " else ""}${param.name.asString()}: ${param.annotatedType.type.render(WITH_TYPE_RENDERING_OPTIONS)}" -} - - -private fun CharSequence.skipSpaces(index: Int): Int = - (index until length).firstOrNull { val c = this[it]; c != ' ' && c != '\t' } ?: this.length - -private fun CharSequence.indexOfSkippingSpace(c: Char, startIndex: Int): Int? { - for (i in startIndex until this.length) { - val currentChar = this[i] - if (c == currentChar) return i - if (currentChar != ' ' && currentChar != '\t') return null - } - return null -} - -internal fun shortenReferencesForFirCompletion(targetFile: KtFile, textRange: TextRange) { - val shortenings = withAllowedResolve { - analyse(targetFile) { - collectPossibleReferenceShortenings(targetFile, textRange) - } - } - shortenings.invokeShortening() -} - -// FIXME: This is a hack, we should think how we can get rid of it -private inline fun withAllowedResolve(action: () -> T): T { - @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) - return hackyAllowRunningOnEdt(action) -} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/context/FirBasicCompletionContext.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/context/FirBasicCompletionContext.kt index ac95711c4b0..6c0a3899c26 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/context/FirBasicCompletionContext.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/context/FirBasicCompletionContext.kt @@ -11,7 +11,7 @@ import com.intellij.codeInsight.completion.PrefixMatcher import com.intellij.openapi.project.Project import org.jetbrains.kotlin.idea.caches.project.IdeaModuleInfo import org.jetbrains.kotlin.idea.caches.project.getModuleInfo -import org.jetbrains.kotlin.idea.completion.KotlinFirLookupElementFactory +import org.jetbrains.kotlin.idea.completion.lookups.factories.KotlinFirLookupElementFactory import org.jetbrains.kotlin.idea.fir.low.level.api.IndexHelper import org.jetbrains.kotlin.idea.project.TargetPlatformDetector import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirCompletionContributorBase.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirCompletionContributorBase.kt index 78bdbb973d6..a469b41a1e9 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirCompletionContributorBase.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirCompletionContributorBase.kt @@ -10,11 +10,11 @@ import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.PrefixMatcher import com.intellij.codeInsight.lookup.LookupElement import com.intellij.openapi.project.Project -import org.jetbrains.kotlin.idea.completion.CallableImportStrategy -import org.jetbrains.kotlin.idea.completion.CallableInsertionStrategy -import org.jetbrains.kotlin.idea.completion.KotlinFirLookupElementFactory import org.jetbrains.kotlin.idea.completion.context.FirBasicCompletionContext import org.jetbrains.kotlin.idea.completion.context.FirRawPositionCompletionContext +import org.jetbrains.kotlin.idea.completion.lookups.CallableImportStrategy +import org.jetbrains.kotlin.idea.completion.lookups.CallableInsertionStrategy +import org.jetbrains.kotlin.idea.completion.lookups.factories.KotlinFirLookupElementFactory import org.jetbrains.kotlin.idea.completion.weighers.Weighers import org.jetbrains.kotlin.idea.fir.low.level.api.IndexHelper import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirImportDirectivePackageMembersCompletionContributor.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirImportDirectivePackageMembersCompletionContributor.kt index a29cf73b2e2..dadf6d59593 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirImportDirectivePackageMembersCompletionContributor.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/FirImportDirectivePackageMembersCompletionContributor.kt @@ -5,12 +5,12 @@ package org.jetbrains.kotlin.idea.completion.contributors -import org.jetbrains.kotlin.idea.completion.CallableImportStrategy -import org.jetbrains.kotlin.idea.completion.CallableInsertionStrategy import org.jetbrains.kotlin.idea.completion.checkers.CompletionVisibilityChecker import org.jetbrains.kotlin.idea.completion.context.FirBasicCompletionContext import org.jetbrains.kotlin.idea.completion.context.FirImportDirectivePositionContext import org.jetbrains.kotlin.idea.completion.contributors.helpers.getStaticScope +import org.jetbrains.kotlin.idea.completion.lookups.CallableImportStrategy +import org.jetbrains.kotlin.idea.completion.lookups.CallableInsertionStrategy import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession internal class FirImportDirectivePackageMembersCompletionContributor( diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/helpers/FirSuperEntriesProvider.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/helpers/FirSuperEntriesProvider.kt index 9eca1107595..8254d8f9d63 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/helpers/FirSuperEntriesProvider.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/helpers/FirSuperEntriesProvider.kt @@ -5,15 +5,12 @@ package org.jetbrains.kotlin.idea.completion.contributors.helpers -import com.intellij.codeInsight.completion.CodeCompletionHandlerBase -import com.intellij.codeInsight.completion.CompletionType import com.intellij.codeInsight.completion.InsertHandler import com.intellij.codeInsight.completion.InsertionContext import com.intellij.codeInsight.lookup.LookupElement -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement -import org.jetbrains.kotlin.idea.completion.shortenReferencesForFirCompletion +import org.jetbrains.kotlin.idea.completion.lookups.shortenReferencesForFirCompletion import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession import org.jetbrains.kotlin.idea.frontend.api.symbols.KtNamedClassOrObjectSymbol import org.jetbrains.kotlin.idea.frontend.api.types.KtClassType diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableImportStrategy.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableImportStrategy.kt new file mode 100644 index 00000000000..9379bb7b805 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableImportStrategy.kt @@ -0,0 +1,57 @@ +/* + * 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.lookups + +import org.jetbrains.kotlin.idea.frontend.api.analyse +import org.jetbrains.kotlin.idea.frontend.api.fir.utils.addImportToFile +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtKotlinPropertySymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtPossibleMemberSymbol +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.psi.KtFile + +internal sealed class CallableImportStrategy { + object DoNothing : CallableImportStrategy() + data class AddImport(val nameToImport: CallableId) : CallableImportStrategy() + data class InsertFqNameAndShorten(val callableId: CallableId) : CallableImportStrategy() +} + +internal fun detectImportStrategy(symbol: KtCallableSymbol): CallableImportStrategy { + if (symbol !is KtPossibleMemberSymbol || symbol.dispatchType != null) return CallableImportStrategy.DoNothing + + val propertyId = symbol.callableIdIfNonLocal ?: return CallableImportStrategy.DoNothing + + return if (symbol.isExtension) { + CallableImportStrategy.AddImport(propertyId) + } else { + CallableImportStrategy.InsertFqNameAndShorten(propertyId) + } +} + +internal fun addCallableImportIfRequired(targetFile: KtFile, nameToImport: CallableId) { + if (!alreadyHasImport(targetFile, nameToImport)) { + addImportToFile(targetFile.project, targetFile, nameToImport) + } +} + +private fun alreadyHasImport(file: KtFile, nameToImport: CallableId): Boolean { + if (file.importDirectives.any { it.importPath?.fqName == nameToImport.asSingleFqName() }) return true + + withAllowedResolve { + analyse(file) { + val scopes = file.getScopeContextForFile().scopes + if (!scopes.mayContainName(nameToImport.callableName)) return false + + return scopes + .getCallableSymbols { it == nameToImport.callableName } + .any { + it is KtKotlinPropertySymbol && it.callableIdIfNonLocal == nameToImport || + it is KtFunctionSymbol && it.callableIdIfNonLocal == nameToImport + } + } + } +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableInsertionStrategy.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableInsertionStrategy.kt new file mode 100644 index 00000000000..3cdf67c67a5 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CallableInsertionStrategy.kt @@ -0,0 +1,11 @@ +/* + * 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.lookups + +internal enum class CallableInsertionStrategy { + AS_CALL, + AS_IDENTIFIER +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CompletionShortNamesRenderer.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CompletionShortNamesRenderer.kt new file mode 100644 index 00000000000..781e94bd418 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/CompletionShortNamesRenderer.kt @@ -0,0 +1,21 @@ +/* + * 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.lookups + +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.components.KtTypeRendererOptions +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtValueParameterSymbol + +internal object CompletionShortNamesRenderer { + fun KtAnalysisSession.renderFunctionParameters(function: KtFunctionSymbol): String = + function.valueParameters.joinToString(", ", "(", ")") { renderFunctionParameter(it) } + + private fun KtAnalysisSession.renderFunctionParameter(param: KtValueParameterSymbol): String = + "${if (param.isVararg) "vararg " else ""}${param.name.asString()}: ${param.annotatedType.type.render(TYPE_RENDERING_OPTIONS)}" + + val TYPE_RENDERING_OPTIONS = KtTypeRendererOptions.SHORT_NAMES +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/KotlinLookupObject.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/KotlinLookupObject.kt new file mode 100644 index 00000000000..003209231ee --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/KotlinLookupObject.kt @@ -0,0 +1,12 @@ +/* + * 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.lookups + +import org.jetbrains.kotlin.name.Name + +internal interface KotlinLookupObject { + val shortName: Name +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/QuotedNamesAwareInsertionHandler.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/QuotedNamesAwareInsertionHandler.kt new file mode 100644 index 00000000000..8290d76e677 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/QuotedNamesAwareInsertionHandler.kt @@ -0,0 +1,26 @@ +/* + * 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.lookups + +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import org.jetbrains.kotlin.idea.completion.handlers.isTextAt +import org.jetbrains.kotlin.renderer.render + +internal open class QuotedNamesAwareInsertionHandler : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val lookupElement = item.`object` as KotlinLookupObject + + val startOffset = context.startOffset + if (startOffset > 0 && context.document.isTextAt(startOffset - 1, "`")) { + context.document.deleteString(startOffset - 1, startOffset) + } + context.document.replaceString(context.startOffset, context.tailOffset, lookupElement.shortName.render()) + + context.commitDocument() + } +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/UniqueLookupObject.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/UniqueLookupObject.kt new file mode 100644 index 00000000000..e1b43c669c6 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/UniqueLookupObject.kt @@ -0,0 +1,11 @@ +/* + * 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.lookups + +/** + * This is a temporary hack to prevent clash of the lookup elements with same names. + */ +internal class UniqueLookupObject \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/ClassLookupElementFactory.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/ClassLookupElementFactory.kt new file mode 100644 index 00000000000..f9e035cdd7c --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/ClassLookupElementFactory.kt @@ -0,0 +1,57 @@ +/* + * 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.lookups.factories + +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.idea.completion.lookups.KotlinLookupObject +import org.jetbrains.kotlin.idea.completion.lookups.shortenReferencesForFirCompletion +import org.jetbrains.kotlin.idea.completion.lookups.withSymbolInfo +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassLikeSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.nameOrAnonymous +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.renderer.render + +class ClassLookupElementFactory { + fun KtAnalysisSession.createLookup(symbol: KtClassLikeSymbol, insertFqName: Boolean = true): LookupElementBuilder { + val name = symbol.nameOrAnonymous + return LookupElementBuilder.create(ClassifierLookupObject(name, symbol.classIdIfNonLocal, insertFqName), name.asString()) + .withInsertHandler(ClassifierInsertionHandler) + .let { withSymbolInfo(symbol, it) } + } +} + + +private data class ClassifierLookupObject( + override val shortName: Name, + val classId: ClassId?, + val insertFqName: Boolean +) : KotlinLookupObject + +/** + * The simplest implementation of the insertion handler for a classifiers. + */ +private object ClassifierInsertionHandler : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val targetFile = context.file as? KtFile ?: return + val lookupObject = item.`object` as ClassifierLookupObject + + if (lookupObject.classId != null && lookupObject.insertFqName) { + val fqName = lookupObject.classId.asSingleFqName() + + context.document.replaceString(context.startOffset, context.tailOffset, fqName.render()) + context.commitDocument() + + shortenReferencesForFirCompletion(targetFile, TextRange(context.startOffset, context.tailOffset)) + } + } +} diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/FunctionLookupElementFactory.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/FunctionLookupElementFactory.kt new file mode 100644 index 00000000000..9f798238aec --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/FunctionLookupElementFactory.kt @@ -0,0 +1,206 @@ +/* + * 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.lookups.factories + +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.Lookup +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.idea.completion.lookups.* +import org.jetbrains.kotlin.idea.completion.lookups.CallableImportStrategy +import org.jetbrains.kotlin.idea.completion.lookups.CallableInsertionStrategy +import org.jetbrains.kotlin.idea.completion.lookups.CompletionShortNamesRenderer +import org.jetbrains.kotlin.idea.completion.lookups.KotlinLookupObject +import org.jetbrains.kotlin.idea.completion.lookups.QuotedNamesAwareInsertionHandler +import org.jetbrains.kotlin.idea.completion.lookups.addCallableImportIfRequired +import org.jetbrains.kotlin.idea.completion.lookups.shortenReferencesForFirCompletion +import org.jetbrains.kotlin.idea.core.asFqNameWithRootPrefixIfNeeded +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.symbols.* +import org.jetbrains.kotlin.idea.frontend.api.types.KtFunctionalType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtTypeArgumentList +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.renderer.render + +internal class FunctionLookupElementFactory { + fun KtAnalysisSession.createLookup( + symbol: KtFunctionSymbol, + importStrategy: CallableImportStrategy, + insertionStrategy: CallableInsertionStrategy + ): LookupElementBuilder? { + val lookupObject = FunctionLookupObject( + symbol.name, + importStrategy = importStrategy, + inputValueArguments = symbol.valueParameters.isNotEmpty(), + insertEmptyLambda = insertLambdaBraces(symbol), + renderedFunctionParameters = with(CompletionShortNamesRenderer) { renderFunctionParameters(symbol) } + ) + + val insertionHandler = when (insertionStrategy) { + CallableInsertionStrategy.AS_CALL -> FunctionInsertionHandler + CallableInsertionStrategy.AS_IDENTIFIER -> QuotedNamesAwareInsertionHandler() + } + + return try { + LookupElementBuilder.create(lookupObject, symbol.name.asString()) + .withTailText(getTailText(symbol), true) + .withTypeText(symbol.annotatedType.type.render(CompletionShortNamesRenderer.TYPE_RENDERING_OPTIONS)) + .withInsertHandler(insertionHandler) + .let { withSymbolInfo(symbol, it) } + } catch (e: Throwable) { + if (e is ControlFlowException) throw e + LOG.error(e) + null + } + } + + private fun KtAnalysisSession.getTailText(symbol: KtFunctionSymbol): String { + return if (insertLambdaBraces(symbol)) " {...}" else with(CompletionShortNamesRenderer) { renderFunctionParameters(symbol) } + } + + private fun KtAnalysisSession.insertLambdaBraces(symbol: KtFunctionSymbol): Boolean { + val singleParam = symbol.valueParameters.singleOrNull() + return singleParam != null && !singleParam.hasDefaultValue && singleParam.annotatedType.type is KtFunctionalType + } + + companion object { + private val LOG = logger() + } +} + +/** + * Simplest lookup object so two lookup elements for the same function will clash. + */ +private data class FunctionLookupObject( + override val shortName: Name, + val importStrategy: CallableImportStrategy, + val inputValueArguments: Boolean, + val insertEmptyLambda: Boolean, + // for distinction between different overloads + private val renderedFunctionParameters: String +) : KotlinLookupObject + + +private object FunctionInsertionHandler : QuotedNamesAwareInsertionHandler() { + private fun addArguments(context: InsertionContext, offsetElement: PsiElement, lookupObject: FunctionLookupObject) { + val completionChar = context.completionChar + if (completionChar == '(') { //TODO: more correct behavior related to braces type + context.setAddCompletionChar(false) + } + + var offset = context.tailOffset + val document = context.document + val editor = context.editor + val project = context.project + val chars = document.charsSequence + + val isSmartEnterCompletion = completionChar == Lookup.COMPLETE_STATEMENT_SELECT_CHAR + val isReplaceCompletion = completionChar == Lookup.REPLACE_SELECT_CHAR + + val (openingBracket, closingBracket) = if (lookupObject.insertEmptyLambda) '{' to '}' else '(' to ')' + + if (isReplaceCompletion) { + val offset1 = chars.skipSpaces(offset) + if (offset1 < chars.length) { + if (chars[offset1] == '<') { + val token = context.file.findElementAt(offset1)!! + if (token.node.elementType == KtTokens.LT) { + val parent = token.parent + /* if type argument list is on multiple lines this is more likely wrong parsing*/ + if (parent is KtTypeArgumentList && parent.getText().indexOf('\n') < 0) { + offset = parent.endOffset + } + } + } + } + } + + var openingBracketOffset = chars.indexOfSkippingSpace(openingBracket, offset) + var closeBracketOffset = openingBracketOffset?.let { chars.indexOfSkippingSpace(closingBracket, it + 1) } + var inBracketsShift = 0 + + if (openingBracketOffset == null) { + if (lookupObject.insertEmptyLambda) { + if (completionChar == ' ' || completionChar == '{') { + context.setAddCompletionChar(false) + } + + if (isInsertSpacesInOneLineFunctionEnabled(context.file)) { + document.insertString(offset, " { }") + inBracketsShift = 1 + } else { + document.insertString(offset, " {}") + } + } else { + if (isSmartEnterCompletion) { + document.insertString(offset, "(") + } else { + document.insertString(offset, "()") + } + } + context.commitDocument() + + openingBracketOffset = document.charsSequence.indexOfSkippingSpace(openingBracket, offset)!! + closeBracketOffset = document.charsSequence.indexOfSkippingSpace(closingBracket, openingBracketOffset + 1) + } + + if (shouldPlaceCaretInBrackets(completionChar, lookupObject) || closeBracketOffset == null) { + editor.caretModel.moveToOffset(openingBracketOffset + 1 + inBracketsShift) + AutoPopupController.getInstance(project)?.autoPopupParameterInfo(editor, offsetElement) + } else { + editor.caretModel.moveToOffset(closeBracketOffset + 1) + } + } + + private fun shouldPlaceCaretInBrackets(completionChar: Char, lookupObject: FunctionLookupObject): Boolean { + if (completionChar == ',' || completionChar == '.' || completionChar == '=') return false + if (completionChar == '(') return true + return lookupObject.inputValueArguments || lookupObject.insertEmptyLambda + } + + // FIXME Should be fetched from language settings (INSERT_WHITESPACES_IN_SIMPLE_ONE_LINE_METHOD), but we do not have them right now + private fun isInsertSpacesInOneLineFunctionEnabled(file: PsiElement) = true + + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val targetFile = context.file as? KtFile ?: return + val lookupObject = item.`object` as FunctionLookupObject + + super.handleInsert(context, item) + + val startOffset = context.startOffset + val element = context.file.findElementAt(startOffset) ?: return + + val importStrategy = lookupObject.importStrategy + if (importStrategy is CallableImportStrategy.InsertFqNameAndShorten) { + context.document.replaceString( + context.startOffset, + context.tailOffset, + importStrategy.callableId.asFqNameWithRootPrefixIfNeeded().render() + ) + context.commitDocument() + + addArguments(context, element, lookupObject) + context.commitDocument() + + shortenReferencesForFirCompletion(targetFile, TextRange(context.startOffset, context.tailOffset)) + } else { + addArguments(context, element, lookupObject) + context.commitDocument() + + if (importStrategy is CallableImportStrategy.AddImport) { + addCallableImportIfRequired(targetFile, importStrategy.nameToImport) + } + } + } +} diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/KotlinFirLookupElementFactory.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/KotlinFirLookupElementFactory.kt new file mode 100644 index 00000000000..2104a881941 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/KotlinFirLookupElementFactory.kt @@ -0,0 +1,59 @@ +/* + * 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.lookups.factories + +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import org.jetbrains.kotlin.idea.completion.lookups.CallableImportStrategy +import org.jetbrains.kotlin.idea.completion.lookups.CallableInsertionStrategy +import org.jetbrains.kotlin.idea.completion.lookups.detectImportStrategy +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.symbols.* +import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtNamedSymbol +import org.jetbrains.kotlin.name.FqName + +internal class KotlinFirLookupElementFactory { + private val classLookupElementFactory = ClassLookupElementFactory() + private val variableLookupElementFactory = VariableLookupElementFactory() + private val functionLookupElementFactory = FunctionLookupElementFactory() + private val typeParameterLookupElementFactory = TypeParameterLookupElementFactory() + private val packagePartLookupElementFactory = PackagePartLookupElementFactory() + + fun KtAnalysisSession.createLookupElement(symbol: KtNamedSymbol): LookupElement? { + return when (symbol) { + is KtCallableSymbol -> createCallableLookupElement( + symbol, + importingStrategy = detectImportStrategy(symbol), + CallableInsertionStrategy.AS_CALL + ) + is KtClassLikeSymbol -> with(classLookupElementFactory) { createLookup(symbol) } + is KtTypeParameterSymbol -> with(typeParameterLookupElementFactory) { createLookup(symbol) } + else -> throw IllegalArgumentException("Cannot create a lookup element for $symbol") + } + } + + fun KtAnalysisSession.createCallableLookupElement( + symbol: KtCallableSymbol, + importingStrategy: CallableImportStrategy, + insertionStrategy: CallableInsertionStrategy, + ): LookupElementBuilder? { + return when (symbol) { + is KtFunctionSymbol -> with(functionLookupElementFactory) { createLookup(symbol, importingStrategy, insertionStrategy) } + is KtVariableLikeSymbol -> with(variableLookupElementFactory) { createLookup(symbol, importingStrategy) } + else -> throw IllegalArgumentException("Cannot create a lookup element for $symbol") + } + } + + fun createPackagePartLookupElement(packagePartFqName: FqName): LookupElement = + packagePartLookupElementFactory.createPackagePartLookupElement(packagePartFqName) + + fun KtAnalysisSession.createLookupElementForClassLikeSymbol(symbol: KtClassLikeSymbol, insertFqName: Boolean = true): LookupElement? { + if (symbol !is KtNamedSymbol) return null + return with(classLookupElementFactory) { createLookup(symbol, insertFqName) } + } +} + + diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/PackagePartLookupElementFactory.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/PackagePartLookupElementFactory.kt new file mode 100644 index 00000000000..815d238e94d --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/PackagePartLookupElementFactory.kt @@ -0,0 +1,47 @@ +/* + * 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.lookups.factories + +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.icons.AllIcons +import org.jetbrains.kotlin.idea.completion.contributors.helpers.addDotAndInvokeCompletion +import org.jetbrains.kotlin.idea.completion.lookups.KotlinLookupObject +import org.jetbrains.kotlin.miniStdLib.letIf +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.renderer.render + +internal class PackagePartLookupElementFactory { + fun createPackagePartLookupElement(packagePartFqName: FqName): LookupElement { + val shortName = packagePartFqName.shortName() + return LookupElementBuilder.create(PackagePartLookupObject(shortName), "${shortName.render()}.") + .withInsertHandler(PackagePartInsertionHandler) + .withIcon(AllIcons.Nodes.Package) + .letIf(!packagePartFqName.parent().isRoot) { + it.appendTailText("(${packagePartFqName.asString()})", true) + } + } +} + + +private data class PackagePartLookupObject( + override val shortName: Name, +) : KotlinLookupObject + + +private object PackagePartInsertionHandler : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val lookupElement = item.`object` as PackagePartLookupObject + val name = lookupElement.shortName.render() + context.document.replaceString(context.startOffset, context.tailOffset, name) + context.commitDocument() + context.addDotAndInvokeCompletion() + } +} + diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/TypeParameterLookupElementFactory.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/TypeParameterLookupElementFactory.kt new file mode 100644 index 00000000000..2545408813d --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/TypeParameterLookupElementFactory.kt @@ -0,0 +1,19 @@ +/* + * 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.lookups.factories + +import com.intellij.codeInsight.lookup.LookupElementBuilder +import org.jetbrains.kotlin.idea.completion.lookups.UniqueLookupObject +import org.jetbrains.kotlin.idea.completion.lookups.withSymbolInfo +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtTypeParameterSymbol + +internal class TypeParameterLookupElementFactory { + fun KtAnalysisSession.createLookup(symbol: KtTypeParameterSymbol): LookupElementBuilder { + return LookupElementBuilder.create(UniqueLookupObject(), symbol.name.asString()) + .let { withSymbolInfo(symbol, it) } + } +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/VariableLookupElementFactory.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/VariableLookupElementFactory.kt new file mode 100644 index 00000000000..33e78aea7b2 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/factories/VariableLookupElementFactory.kt @@ -0,0 +1,93 @@ +/* + * 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.lookups.factories + +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.idea.completion.lookups.* +import org.jetbrains.kotlin.idea.completion.lookups.CallableImportStrategy +import org.jetbrains.kotlin.idea.completion.lookups.KotlinLookupObject +import org.jetbrains.kotlin.idea.completion.lookups.addCallableImportIfRequired +import org.jetbrains.kotlin.idea.completion.lookups.detectImportStrategy +import org.jetbrains.kotlin.idea.completion.lookups.shortenReferencesForFirCompletion +import org.jetbrains.kotlin.idea.core.asFqNameWithRootPrefixIfNeeded +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.components.KtTypeRendererOptions +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSyntheticJavaPropertySymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtVariableLikeSymbol +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.renderer.render + +internal class VariableLookupElementFactory { + fun KtAnalysisSession.createLookup( + symbol: KtVariableLikeSymbol, + importStrategy: CallableImportStrategy = detectImportStrategy(symbol) + ): LookupElementBuilder { + val lookupObject = VariableLookupObject( + symbol.name, + importStrategy = importStrategy + ) + + return LookupElementBuilder.create(lookupObject, symbol.name.asString()) + .withTypeText(symbol.annotatedType.type.render(KtTypeRendererOptions.SHORT_NAMES)) + .markIfSyntheticJavaProperty(symbol) + .withInsertHandler(VariableInsertionHandler) + .let { withSymbolInfo(symbol, it) } + } + + private fun LookupElementBuilder.markIfSyntheticJavaProperty(symbol: KtVariableLikeSymbol): LookupElementBuilder = when (symbol) { + is KtSyntheticJavaPropertySymbol -> { + val getterName = symbol.javaGetterName.asString() + val setterName = symbol.javaSetterName?.asString() + this.withTailText((" (from ${buildSyntheticPropertyTailText(getterName, setterName)})")) + .withLookupStrings(listOfNotNull(getterName, setterName)) + } + else -> this + } + + private fun buildSyntheticPropertyTailText(getterName: String, setterName: String?): String = + if (setterName != null) "$getterName()/$setterName()" else "$getterName()" +} + +/** + * Simplest lookup object so two lookup elements for the same property will clash. + */ +private data class VariableLookupObject( + override val shortName: Name, + val importStrategy: CallableImportStrategy +) : KotlinLookupObject + + +private object VariableInsertionHandler : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val targetFile = context.file as? KtFile ?: return + val lookupObject = item.`object` as VariableLookupObject + + when (val importStrategy = lookupObject.importStrategy) { + is CallableImportStrategy.AddImport -> { + addCallableImportIfRequired(targetFile, importStrategy.nameToImport) + } + + is CallableImportStrategy.InsertFqNameAndShorten -> { + context.document.replaceString( + context.startOffset, + context.tailOffset, + importStrategy.callableId.asFqNameWithRootPrefixIfNeeded().render() + ) + + context.commitDocument() + shortenReferencesForFirCompletion(targetFile, TextRange(context.startOffset, context.tailOffset)) + } + + is CallableImportStrategy.DoNothing -> { + } + } + } +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/utils.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/utils.kt new file mode 100644 index 00000000000..076a650c884 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/lookups/utils.kt @@ -0,0 +1,52 @@ +/* + * 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.lookups + +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.idea.completion.KotlinFirIconProvider +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.analyse +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSymbol +import org.jetbrains.kotlin.idea.frontend.api.tokens.HackToForceAllowRunningAnalyzeOnEDT +import org.jetbrains.kotlin.idea.frontend.api.tokens.hackyAllowRunningOnEdt +import org.jetbrains.kotlin.psi.KtFile + +internal fun KtAnalysisSession.withSymbolInfo( + symbol: KtSymbol, + elementBuilder: LookupElementBuilder +): LookupElementBuilder = elementBuilder + .withPsiElement(symbol.psi) // TODO check if it is a heavy operation and should be postponed + .withIcon(KotlinFirIconProvider.getIconFor(symbol)) + + +// FIXME: This is a hack, we should think how we can get rid of it +internal inline fun withAllowedResolve(action: () -> T): T { + @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) + return hackyAllowRunningOnEdt(action) +} + +internal fun CharSequence.skipSpaces(index: Int): Int = + (index until length).firstOrNull { val c = this[it]; c != ' ' && c != '\t' } ?: this.length + +internal fun CharSequence.indexOfSkippingSpace(c: Char, startIndex: Int): Int? { + for (i in startIndex until this.length) { + val currentChar = this[i] + if (c == currentChar) return i + if (currentChar != ' ' && currentChar != '\t') return null + } + return null +} + +internal fun shortenReferencesForFirCompletion(targetFile: KtFile, textRange: TextRange) { + val shortenings = withAllowedResolve { + analyse(targetFile) { + collectPossibleReferenceShortenings(targetFile, textRange) + } + } + shortenings.invokeShortening() +} +