[REPL] Fix completion for function arguments

This commit is contained in:
Ilya Muradyan
2021-06-06 09:00:52 +03:00
parent 0bc34f0ff9
commit 8be5f009f1
2 changed files with 279 additions and 179 deletions
@@ -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
@@ -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 <reified T> 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<SourceCodeCompletionVariant> gen@{
val filterOutShadowedDescriptors = configuration[ScriptCompilationConfiguration.completion.filterOutShadowedDescriptors]!!
val nameFilter = configuration[ScriptCompilationConfiguration.completion.nameFilter]!!
private val getDescriptorsQualified = ResultGetter { element, options ->
val expression = element.thisOrParent<KtQualifiedExpression>() ?: return@ResultGetter null
val element = getElementAt(cursor)
val receiverExpression = expression.receiverExpression
val expressionType = bindingContext.get(
BindingContext.EXPRESSION_TYPE_INFO,
receiverExpression
)?.type
var descriptors: Collection<DeclarationDescriptor>? = 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<KtSimpleNameExpression>() ?: 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<KtValueArgument>()
val containingCall = containingArgument?.getParentOfType<KtCallExpression>(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<KtCallExpression>(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<DeclarationDescriptor>().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<SourceCodeCompletionVariant> {
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<DeclarationDescriptor>)
.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<SourceCodeCompletionVariant> {
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<DeclarationDescriptor> = mutableListOf(),
var variants: Sequence<SourceCodeCompletionVariant>? = null,
var sortNeeded: Boolean = true,
var targetElement: PsiElement,
val containingCallParameters: MutableList<ValueParameterDescriptor> = 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<ValueParameterDescriptor>
): 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) {