KT-35258 Add better completion for MixedNamedArgumentsInTheirOwnPosition feature

- When named parameter is on its own position, the completion of the next argument will not be limited by named arguments only
- The completion will not suggest already used arguments names
- ^KT-35258 Fixed
This commit is contained in:
Roman Golyshev
2019-12-25 19:40:10 +03:00
committed by Roman Golyshev
parent bd713db70e
commit 4b50fb18fb
9 changed files with 106 additions and 4 deletions
@@ -105,7 +105,7 @@ class BasicCompletionSession(
if (OPERATOR_NAME.isApplicable())
return OPERATOR_NAME
if (NamedArgumentCompletion.isOnlyNamedArgumentExpected(nameExpression)) {
if (NamedArgumentCompletion.isOnlyNamedArgumentExpected(nameExpression, resolutionFacade)) {
return NAMED_ARGUMENTS_ONLY
}
@@ -9,28 +9,42 @@ 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 org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.completion.handlers.WithTailInsertHandler
import org.jetbrains.kotlin.idea.core.ArgumentPositionData
import org.jetbrains.kotlin.idea.core.ExpectedInfo
import org.jetbrains.kotlin.idea.project.languageVersionSettings
import org.jetbrains.kotlin.idea.resolve.ResolutionFacade
import org.jetbrains.kotlin.idea.util.CallType
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtCallElement
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.psi.ValueArgument
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.resolve.calls.callUtil.getParameterForArgument
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.types.KotlinType
import java.util.*
object NamedArgumentCompletion {
fun isOnlyNamedArgumentExpected(nameExpression: KtSimpleNameExpression): Boolean {
fun isOnlyNamedArgumentExpected(nameExpression: KtSimpleNameExpression, resolutionFacade: ResolutionFacade): Boolean {
val thisArgument = nameExpression.parent as? KtValueArgument ?: return false
if (thisArgument.isNamed()) return false
val callElement = thisArgument.getStrictParentOfType<KtCallElement>() ?: return false
val argumentsBeforeThis = callElement.valueArguments.takeWhile { it != thisArgument }
return callElement.valueArguments.takeWhile { it != thisArgument }.any { it.isNamed() }
if (!nameExpression.languageVersionSettings.supportsFeature(LanguageFeature.MixedNamedArgumentsInTheirOwnPosition)) {
return argumentsBeforeThis.any { it.isNamed() }
}
val resolvedCall = callElement.resolveToCall(resolutionFacade) ?: return false
return argumentsBeforeThis.any { it.isNamed() && !it.placedOnItsOwnPositionInCall(resolvedCall) }
}
fun complete(collector: LookupElementsCollector, expectedInfos: Collection<ExpectedInfo>, callType: CallType<*>) {
@@ -68,3 +82,23 @@ object NamedArgumentCompletion {
}
}
}
/**
* Checks whether argument in the [resolvedCall] is on the same position as it listed in the callable definition.
*
* It is always true for the positional arguments, but may be untrue for the named arguments.
*
* ```
* fun foo(a: Int, b: Int, c: Int, d: Int) {}
*
* foo(
* 10, // true
* b = 10, // true, possible since Kotlin 1.4 with `MixedNamedArgumentsInTheirOwnPosition` feature
* d = 30, // false, 3 vs 4
* c = 40 // false, 4 vs 3
* )
* ```
*/
private fun ValueArgument.placedOnItsOwnPositionInCall(resolvedCall: ResolvedCall<out CallableDescriptor>): Boolean {
return resolvedCall.getParameterForArgument(this)?.index == resolvedCall.call.valueArguments.indexOf(this)
}
@@ -63,7 +63,7 @@ class SmartCompletionSession(
get() = smartCompletion?.expectedInfos ?: emptyList()
override fun doComplete() {
if (nameExpression != null && NamedArgumentCompletion.isOnlyNamedArgumentExpected(nameExpression)) {
if (nameExpression != null && NamedArgumentCompletion.isOnlyNamedArgumentExpected(nameExpression, resolutionFacade)) {
NamedArgumentCompletion.complete(collector, expectedInfos, callTypeAndReceiver.callType)
return
}
@@ -0,0 +1,10 @@
fun foo(p1: Int, p2: Int, p3: Long) {}
fun usage(param: Long) {
foo(p2 = 10, <caret>)
}
// LANGUAGE_VERSION: 1.4
// EXIST: { itemText: "p1 =" }
// EXIST: { itemText: "p3 =" }
// NOTHING_ELSE
@@ -0,0 +1,11 @@
fun foo(p1: Int, p2: Int, p3: Long) {}
fun usage(param: Long) {
foo(p1 = 10, <caret>)
}
// LANGUAGE_VERSION: 1.4
// EXIST: param
// EXIST: { itemText: "p2 =" }
// EXIST: { itemText: "p3 =" }
// ABSENT: { itemText: "p1 =" }
@@ -0,0 +1,13 @@
fun foo(p1: Int, p2: Int, p3: Int, p4: Int, p5: Int) {}
fun usage(param: Int) {
foo(0, p2 = 10, 20, p4 = 30, <caret>)
}
// LANGUAGE_VERSION: 1.4
// EXIST: param
// EXIST: { itemText: "p5 =" }
// ABSENT: { itemText: "p1 =" }
// ABSENT: { itemText: "p2 =" }
// ABSENT: { itemText: "p3 =" }
// ABSENT: { itemText: "p4 =" }
@@ -1799,6 +1799,21 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes
runTest("idea/idea-completion/testData/basic/common/namedArguments/InParameterExpression.kt");
}
@TestMetadata("NamedArgumentOnIncorrectPosition.kt")
public void testNamedArgumentOnIncorrectPosition() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentOnIncorrectPosition.kt");
}
@TestMetadata("NamedArgumentOnItsOwnPosition.kt")
public void testNamedArgumentOnItsOwnPosition() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentOnItsOwnPosition.kt");
}
@TestMetadata("NamedArgumentOnItsOwnPosition2.kt")
public void testNamedArgumentOnItsOwnPosition2() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentOnItsOwnPosition2.kt");
}
@TestMetadata("NamedArgumentsFromOverloads.kt")
public void testNamedArgumentsFromOverloads() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentsFromOverloads.kt");
@@ -1799,6 +1799,21 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT
runTest("idea/idea-completion/testData/basic/common/namedArguments/InParameterExpression.kt");
}
@TestMetadata("NamedArgumentOnIncorrectPosition.kt")
public void testNamedArgumentOnIncorrectPosition() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentOnIncorrectPosition.kt");
}
@TestMetadata("NamedArgumentOnItsOwnPosition.kt")
public void testNamedArgumentOnItsOwnPosition() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentOnItsOwnPosition.kt");
}
@TestMetadata("NamedArgumentOnItsOwnPosition2.kt")
public void testNamedArgumentOnItsOwnPosition2() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentOnItsOwnPosition2.kt");
}
@TestMetadata("NamedArgumentsFromOverloads.kt")
public void testNamedArgumentsFromOverloads() throws Exception {
runTest("idea/idea-completion/testData/basic/common/namedArguments/NamedArgumentsFromOverloads.kt");
@@ -62,6 +62,10 @@ fun Call.mapArgumentsToParameters(targetDescriptor: CallableDescriptor): Map<Val
val parameter = parametersByName[argumentName]
if (parameter != null) {
map[argument] = parameter
if (parameter.index == positionalArgumentIndex) {
positionalArgumentIndex++
continue
}
}
positionalArgumentIndex = null
} else {