diff --git a/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt b/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt index 14d4e92447a..9f493c5f19d 100644 --- a/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt +++ b/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt @@ -113,10 +113,13 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { fun testFunctionArgumentNames() = test { run { doCompile - code = """fun _sf(_someInt: Int = 42, _someString: String = "s") = 1""" + code = """ + fun _sf(_someInt: Int = 42, _someString: String = "s") = 1 + fun String.f(_bar: Int) = _bar + class C(val _xyz: Int) + """.trimIndent() } run { - doComplete code = """_sf(_s""" cursor = code.length expect { @@ -125,6 +128,38 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { addCompletion("_someString = ", "_someString", "String", "parameter") } } + run { + code = """ "my string".f(_b""" + cursor = code.length + expect { + addCompletion("_bar = ", "_bar", "Int", "parameter") + } + } + run { + code = "C(_x" + cursor = code.length + expect { + addCompletion("_xyz = ", "_xyz", "Int", "parameter") + } + } + } + + @Test + fun testCompletionInsideFunctions() = test { + run { + val statement = "val a = _f" + code = """ + fun dontCompleteMe(_foo: Int, bar: String) { + val _foo2 = "" + $statement + } + """.trimIndent() + cursor = code.indexOf(statement) + statement.length + expect { + addCompletion("_foo2", "_foo2", "String", "property") + addCompletion("_foo", "_foo", "Int", "parameter") + } + } } @Test diff --git a/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt index 96d16c606e4..dc1d5cfb82f 100644 --- a/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt +++ b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt @@ -31,7 +31,6 @@ import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.annotations.argumentValue import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter -import org.jetbrains.kotlin.resolve.scopes.LexicalScope import org.jetbrains.kotlin.resolve.scopes.MemberScope.Companion.ALL_NAME_FILTER import org.jetbrains.kotlin.scripting.ide_services.compiler.completion import org.jetbrains.kotlin.scripting.ide_services.compiler.filterOutShadowedDescriptors @@ -41,8 +40,6 @@ import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.asFlexibleType import org.jetbrains.kotlin.types.isFlexible import java.io.File -import java.lang.IllegalArgumentException -import java.util.* import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.SourceCodeCompletionVariant @@ -72,6 +69,12 @@ fun getKJvmCompletion( fun prepareCodeForCompletion(code: String, cursor: Int) = code.substring(0, cursor) + KJvmReplCompleter.INSERTED_STRING + code.substring(cursor) +private inline fun PsiElement.thisOrParent() = when { + this is T -> this + this.parent is T -> (this.parent as T) + else -> null +} + private class KJvmReplCompleter( private val ktScript: KtFile, private val bindingContext: BindingContext, @@ -89,148 +92,162 @@ private class KJvmReplCompleter( return element } - fun getCompletion() = sequence gen@{ - val filterOutShadowedDescriptors = configuration[ScriptCompilationConfiguration.completion.filterOutShadowedDescriptors]!! - val nameFilter = configuration[ScriptCompilationConfiguration.completion.nameFilter]!! + private val getDescriptorsQualified = ResultGetter { element, options -> + val expression = element.thisOrParent() ?: return@ResultGetter null - val element = getElementAt(cursor) + val receiverExpression = expression.receiverExpression + val expressionType = bindingContext.get( + BindingContext.EXPRESSION_TYPE_INFO, + receiverExpression + )?.type - var descriptors: Collection? = null - var isTipsManagerCompletion = true - var isSortNeeded = true + DescriptorsResult(targetElement = expression).apply { + if (expressionType != null) { + sortNeeded = false + descriptors.addAll( + getVariantsHelper { true } + .getReferenceVariants( + receiverExpression, + CallTypeAndReceiver.DOT(receiverExpression), + DescriptorKindFilter.ALL, + ALL_NAME_FILTER, + filterOutShadowed = options.filterOutShadowedDescriptors, + ) + ) + } + } + } - if (element == null) - return@gen + private val getDescriptorsSimple = ResultGetter { element, options -> + val expression = element.thisOrParent() ?: return@ResultGetter null - val simpleExpression = when { - element is KtSimpleNameExpression -> element - element.parent is KtSimpleNameExpression -> element.parent as KtSimpleNameExpression - else -> null + val result = DescriptorsResult(targetElement = expression) + val inDescriptor: DeclarationDescriptor = expression.getResolutionScope(bindingContext, resolutionFacade).ownerDescriptor + val prefix = element.text.substring(0, cursor - element.startOffset) + + val elementParent = element.parent + if (prefix.isEmpty() && elementParent is KtBinaryExpression) { + val parentChildren = elementParent.children + if (parentChildren.size == 3 && + parentChildren[1] is KtOperationReferenceExpression && + parentChildren[1].text == INSERTED_STRING + ) return@ResultGetter result } - if (simpleExpression != null) { - val inDescriptor: DeclarationDescriptor = simpleExpression.getResolutionScope(bindingContext, resolutionFacade).ownerDescriptor - val prefix = element.text.substring(0, cursor - element.startOffset) + val containingArgument = expression.thisOrParent() + val containingCall = containingArgument?.getParentOfType(true) + val containingQualifiedExpression = containingCall?.parent as? KtDotQualifiedExpression + val containingCallId = containingCall?.calleeExpression?.text + fun Name.test(checkAgainstContainingCall: Boolean): Boolean { + if (isSpecial) return false + if (options.nameFilter(identifier, prefix)) return true + return checkAgainstContainingCall && containingCallId?.let { options.nameFilter(identifier, it) } == true + } - val elementParent = element.parent - if (prefix.isEmpty() && elementParent is KtBinaryExpression) { - val parentChildren = elementParent.children - if (parentChildren.size == 3 && - parentChildren[1] is KtOperationReferenceExpression && - parentChildren[1].text == INSERTED_STRING - ) return@gen - } + DescriptorsResult(targetElement = element).apply { + sortNeeded = false - val containingCallId = simpleExpression.getParentOfType(true)?.calleeExpression?.text - fun Name.test(checkAgainstContainingCall: Boolean): Boolean { - if (isSpecial) return false - if (nameFilter(identifier, prefix)) return true - return checkAgainstContainingCall && containingCallId?.let { nameFilter(identifier, it) } == true - } + descriptors.apply { + fun addParameters(descriptor: DeclarationDescriptor) { + if (containingCallId == descriptor.name.identifier) { + val params = when (descriptor) { + is CallableDescriptor -> descriptor.valueParameters + is ClassDescriptor -> descriptor.constructors.flatMap { it.valueParameters } + else -> emptyList() + } + val valueParams = params.filter { it.name.test(false) } + addAll(valueParams) + containingCallParameters.addAll(valueParams) + } + } - isSortNeeded = false - descriptors = ArrayList().also { result -> - ReferenceVariantsHelper( - bindingContext, - resolutionFacade, - moduleDescriptor, + getVariantsHelper( VisibilityFilter(inDescriptor) ).getReferenceVariants( - simpleExpression, + expression, DescriptorKindFilter.ALL, { it.test(true) }, filterOutJavaGettersAndSetters = true, - filterOutShadowed = filterOutShadowedDescriptors, // setting to true makes it slower up to 4 times + filterOutShadowed = options.filterOutShadowedDescriptors, // setting to true makes it slower up to 4 times excludeNonInitializedVariable = true, useReceiverType = null ).forEach { descriptor -> - if (descriptor.name.test(false)) result.add(descriptor) - if (descriptor is CallableDescriptor && containingCallId == descriptor.name.identifier) { - descriptor.valueParameters.filterTo(result) { it.name.test(false) } - } + if (descriptor.name.test(false)) add(descriptor) + addParameters(descriptor) } - } - } else if (element is KtStringTemplateExpression) { - if (element.hasInterpolation()) { - return@gen - } - - val stringVal = element.entries.joinToString("") { - val t = it.text - if (it.startOffset <= cursor && cursor <= it.endOffset) { - val s = cursor - it.startOffset - val e = s + INSERTED_STRING.length - t.substring(0, s) + t.substring(e) - } else t - } - - val separatorIndex = stringVal.lastIndexOfAny(charArrayOf('/', '\\')) - val dir = if (separatorIndex != -1) { - stringVal.substring(0, separatorIndex + 1) - } else { - "." - } - val namePrefix = stringVal.substring(separatorIndex + 1) - - val file = File(dir) - - file.listFiles { p, f -> p == file && f.startsWith(namePrefix, true) }?.forEach { - yield(SourceCodeCompletionVariant(it.name, it.name, "file", "file")) - } - - return@gen - - } else { - isTipsManagerCompletion = false - val resolutionScope: LexicalScope? - val parent = element.parent - - val qualifiedExpression = when { - element is KtQualifiedExpression -> { - isTipsManagerCompletion = true - element + if (containingQualifiedExpression != null) { + val receiverExpression = containingQualifiedExpression.receiverExpression + getVariantsHelper { true } + .getReferenceVariants( + receiverExpression, + CallTypeAndReceiver.DOT(receiverExpression), + DescriptorKindFilter.CALLABLES, + ALL_NAME_FILTER, + filterOutShadowed = options.filterOutShadowedDescriptors, + ) + .forEach { descriptor -> + addParameters(descriptor) + } } - parent is KtQualifiedExpression -> parent - else -> null - } - - if (qualifiedExpression != null) { - val receiverExpression = qualifiedExpression.receiverExpression - val expressionType = bindingContext.get( - BindingContext.EXPRESSION_TYPE_INFO, - receiverExpression - )?.type - if (expressionType != null) { - isSortNeeded = false - descriptors = ReferenceVariantsHelper( - bindingContext, - resolutionFacade, - moduleDescriptor, - { true } - ).getReferenceVariants( - receiverExpression, - CallTypeAndReceiver.DOT(receiverExpression), - DescriptorKindFilter.ALL, - ALL_NAME_FILTER, - filterOutShadowed = filterOutShadowedDescriptors, - ) - } - } else { - resolutionScope = bindingContext.get( - BindingContext.LEXICAL_SCOPE, - element as KtExpression? - ) - descriptors = (resolutionScope?.getContributedDescriptors( - DescriptorKindFilter.ALL, - ALL_NAME_FILTER - ) - ?: return@gen) } } + } - if (descriptors != null) { - val targetElement = if (isTipsManagerCompletion) element else element.parent + private val getDescriptorsString = ResultGetter { element, _ -> + if (element !is KtStringTemplateExpression) return@ResultGetter null + + val stringVal = element.entries.joinToString("") { + val t = it.text + if (it.startOffset <= cursor && cursor <= it.endOffset) { + val s = cursor - it.startOffset + val e = s + INSERTED_STRING.length + t.substring(0, s) + t.substring(e) + } else t + } + + val separatorIndex = stringVal.lastIndexOfAny(charArrayOf('/', '\\')) + val dir = if (separatorIndex != -1) { + stringVal.substring(0, separatorIndex + 1) + } else { + "." + } + val namePrefix = stringVal.substring(separatorIndex + 1) + + val file = File(dir) + DescriptorsResult(targetElement = element).also { result -> + result.variants = sequence { + file.listFiles { p, f -> p == file && f.startsWith(namePrefix, true) }?.forEach { + yield(SourceCodeCompletionVariant(it.name, it.name, "file", "file")) + } + } + } + } + + private val getDescriptorsDefault = ResultGetter { element, _ -> + val resolutionScope = bindingContext.get( + BindingContext.LEXICAL_SCOPE, + element as KtExpression? + ) + DescriptorsResult(targetElement = element).also { result -> + resolutionScope?.getContributedDescriptors( + DescriptorKindFilter.ALL, + ALL_NAME_FILTER + )?.let { descriptors -> + result.descriptors.addAll(descriptors) + } + } + } + + private fun renderResult( + element: PsiElement, + options: DescriptorsOptions, + result: DescriptorsResult? + ): Sequence { + if (result == null) return emptySequence() + result.variants?.let { return it } + + with(result) { val prefixEnd = cursor - targetElement.startOffset var prefix = targetElement.text.substring(0, prefixEnd) @@ -243,67 +260,109 @@ private class KJvmReplCompleter( prefix.substring(0, cursorWithinElement) } - if (descriptors !is ArrayList<*>) { - descriptors = ArrayList(descriptors) - } - - (descriptors as ArrayList) - .map { - val presentation = - getPresentation( - it - ) - Triple(it, presentation, (presentation.presentableText + presentation.tailText).lowercase()) - } - .let { - if (isSortNeeded) it.sortedBy { descTriple -> descTriple.third } else it - } - .forEach { resultTriple -> - val descriptor = resultTriple.first - val (rawName, presentableText, tailText, completionText) = resultTriple.second - if (nameFilter(rawName, prefix)) { - val fullName: String = - formatName( - presentableText + return sequence { + descriptors + .map { + val presentation = + getPresentation( + it, result.containingCallParameters ) - val deprecationLevel = descriptor.annotations - .findAnnotation(FqName("kotlin.Deprecated")) - ?.let { annotationDescriptor -> - val valuePair = annotationDescriptor.argumentValue("level")?.value as? Pair<*, *> - val valueClass = (valuePair?.first as? ClassId)?.takeIf { DeprecationLevel::class.classId == it } - val valueName = (valuePair?.second as? Name)?.identifier - if (valueClass == null || valueName == null) return@let DeprecationLevel.WARNING - DeprecationLevel.valueOf(valueName) - } - yield( - SourceCodeCompletionVariant( - completionText, - fullName, - tailText, - getIconFromDescriptor( - descriptor - ), - deprecationLevel, - ) - ) + Triple(it, presentation, (presentation.presentableText + presentation.tailText).lowercase()) + } + .let { + if (sortNeeded) it.sortedBy { descTriple -> descTriple.third } else it + } + .forEach { resultTriple -> + val descriptor = resultTriple.first + val (rawName, presentableText, tailText, completionText) = resultTriple.second + if (options.nameFilter(rawName, prefix)) { + val fullName: String = + formatName( + presentableText + ) + val deprecationLevel = descriptor.annotations + .findAnnotation(FqName("kotlin.Deprecated")) + ?.let { annotationDescriptor -> + val valuePair = annotationDescriptor.argumentValue("level")?.value as? Pair<*, *> + val valueClass = (valuePair?.first as? ClassId)?.takeIf { DeprecationLevel::class.classId == it } + val valueName = (valuePair?.second as? Name)?.identifier + if (valueClass == null || valueName == null) return@let DeprecationLevel.WARNING + DeprecationLevel.valueOf(valueName) + } + yield( + SourceCodeCompletionVariant( + completionText, + fullName, + tailText, + getIconFromDescriptor( + descriptor + ), + deprecationLevel, + ) + ) + } } - } - yieldAll( - keywordsCompletionVariants( - KtTokens.KEYWORDS, - prefix + yieldAll( + keywordsCompletionVariants( + KtTokens.KEYWORDS, + prefix + ) ) - ) - yieldAll( - keywordsCompletionVariants( - KtTokens.SOFT_KEYWORDS, - prefix + yieldAll( + keywordsCompletionVariants( + KtTokens.SOFT_KEYWORDS, + prefix + ) ) - ) + } } } + fun getCompletion(): Sequence { + val filterOutShadowedDescriptors = configuration[ScriptCompilationConfiguration.completion.filterOutShadowedDescriptors]!! + val nameFilter = configuration[ScriptCompilationConfiguration.completion.nameFilter]!! + val options = DescriptorsOptions( + nameFilter, filterOutShadowedDescriptors + ) + + val element = getElementAt(cursor) ?: return emptySequence() + + val descriptorsGetters = listOf( + getDescriptorsSimple, + getDescriptorsString, + getDescriptorsQualified, + getDescriptorsDefault, + ) + + val result = descriptorsGetters.firstNotNullOfOrNull { it.get(element, options) } + return renderResult(element, options, result) + } + + private fun getVariantsHelper(visibilityFilter: (DeclarationDescriptor) -> Boolean) = ReferenceVariantsHelper( + bindingContext, + resolutionFacade, + moduleDescriptor, + visibilityFilter, + ) + + private fun interface ResultGetter { + fun get(element: PsiElement, options: DescriptorsOptions): DescriptorsResult? + } + + private class DescriptorsResult( + val descriptors: MutableList = mutableListOf(), + var variants: Sequence? = null, + var sortNeeded: Boolean = true, + var targetElement: PsiElement, + val containingCallParameters: MutableList = mutableListOf(), + ) + + private class DescriptorsOptions( + val nameFilter: (String, String) -> Boolean, + val filterOutShadowedDescriptors: Boolean, + ) + private class VisibilityFilter( private val inDescriptor: DeclarationDescriptor ) : (DeclarationDescriptor) -> Boolean { @@ -396,7 +455,10 @@ private class KJvmReplCompleter( val completionText: String ) - fun getPresentation(descriptor: DeclarationDescriptor): DescriptorPresentation { + fun getPresentation( + descriptor: DeclarationDescriptor, + callParameters: Collection + ): DescriptorPresentation { val rawDescriptorName = descriptor.name.asString() val descriptorName = rawDescriptorName.quoteIfNeeded() var presentableText = descriptorName @@ -426,7 +488,10 @@ private class KJvmReplCompleter( val outType = descriptor.type typeText = RENDERER.renderType(outType) - if (descriptor is ValueParameterDescriptor) { + if ( + descriptor is ValueParameterDescriptor && + callParameters.contains(descriptor) + ) { completionText = "$rawDescriptorName = " } } else if (descriptor is ClassDescriptor) {