From acfd308a7e40a21c87a0d54115d11b1b02bdf080 Mon Sep 17 00:00:00 2001 From: Valentin Kipyatkov Date: Fri, 28 Nov 2014 17:48:30 +0300 Subject: [PATCH] Code completion: graying members after dot completion for nullable value --- .../codeInsight/ReferenceVariantsHelper.kt | 69 ++++++++++++++----- .../plugin/completion/CompletionSession.kt | 8 ++- .../plugin/completion/LookupElementFactory.kt | 42 +++++++++-- .../completion/basic/common/AfterNullable.kt | 14 ++++ .../basic/common/AfterNullableAutoCast.kt | 16 +++++ .../basic/common/AfterNullableAutoCast2.kt | 30 ++++++++ .../basic/common/SafeCallAfterNullable.kt | 14 ++++ .../completion/ExpectedCompletionUtils.java | 9 +++ .../JSBasicCompletionTestGenerated.java | 24 +++++++ .../JvmBasicCompletionTestGenerated.java | 24 +++++++ 10 files changed, 226 insertions(+), 24 deletions(-) create mode 100644 idea/testData/completion/basic/common/AfterNullable.kt create mode 100644 idea/testData/completion/basic/common/AfterNullableAutoCast.kt create mode 100644 idea/testData/completion/basic/common/AfterNullableAutoCast2.kt create mode 100644 idea/testData/completion/basic/common/SafeCallAfterNullable.kt diff --git a/idea/ide-common/src/org/jetbrains/jet/plugin/codeInsight/ReferenceVariantsHelper.kt b/idea/ide-common/src/org/jetbrains/jet/plugin/codeInsight/ReferenceVariantsHelper.kt index f62a3c0d64e..63dd73bc072 100644 --- a/idea/ide-common/src/org/jetbrains/jet/plugin/codeInsight/ReferenceVariantsHelper.kt +++ b/idea/ide-common/src/org/jetbrains/jet/plugin/codeInsight/ReferenceVariantsHelper.kt @@ -36,12 +36,28 @@ import org.jetbrains.jet.plugin.util.extensionsUtils.isExtensionCallable import org.jetbrains.jet.lang.types.JetType import org.jetbrains.jet.lang.types.TypeUtils import org.jetbrains.jet.lang.types.checker.JetTypeChecker +import org.jetbrains.jet.lexer.JetTokens public class ReferenceVariantsHelper( private val context: BindingContext, private val visibilityFilter: (DeclarationDescriptor) -> Boolean ) { + public enum class CallType { + NORMAL + INFIX + SAFE + } + + public data class ReceiversData( + public val receivers: Collection, + public val callType: CallType + ) { + class object { + val Empty = ReceiversData(listOf(), CallType.NORMAL) + } + } + public fun getReferenceVariants( expression: JetSimpleNameExpression, kindFilter: DescriptorKindFilter, @@ -65,11 +81,11 @@ public class ReferenceVariantsHelper( return resolutionScope.getDescriptorsFiltered(restrictedFilter, nameFilter) } - val receiverExpression = getReferenceVariantsReceiver(expression) - if (receiverExpression != null) { - val isInfixCall = parent is JetBinaryExpression + val pair = getReferenceVariantsReceiver(expression) + if (pair != null) { + val (receiverExpression, callType) = pair fun filterIfInfix(descriptor: DeclarationDescriptor) - = if (isInfixCall) descriptor is SimpleFunctionDescriptor && descriptor.getValueParameters().size == 1 else true + = if (callType == CallType.INFIX) descriptor is SimpleFunctionDescriptor && descriptor.getValueParameters().size == 1 else true // Process as call expression val descriptors = HashSet() @@ -93,7 +109,7 @@ public class ReferenceVariantsHelper( variant.getMemberScope().getDescriptorsFiltered(mask, nameFilter).filterTo(descriptors, ::filterIfInfix) } - descriptors.addCallableExtensions(resolutionScope, receiverValue, dataFlowInfo, isInfixCall, kindFilter, nameFilter) + descriptors.addCallableExtensions(resolutionScope, receiverValue, dataFlowInfo, callType == CallType.INFIX, kindFilter, nameFilter) } return descriptors @@ -120,15 +136,16 @@ public class ReferenceVariantsHelper( } } - public fun getReferenceVariantsReceivers(expression: JetSimpleNameExpression): Collection { - val receiverExpression = getReferenceVariantsReceiver(expression) - if (receiverExpression != null) { - val expressionType = context[BindingContext.EXPRESSION_TYPE, receiverExpression] ?: return listOf() - return listOf(ExpressionReceiver(receiverExpression, expressionType)) + public fun getReferenceVariantsReceivers(expression: JetSimpleNameExpression): ReceiversData { + val receiverData = getReferenceVariantsReceiver(expression) + if (receiverData != null) { + val receiverExpression = receiverData.first + val expressionType = context[BindingContext.EXPRESSION_TYPE, receiverExpression] ?: return ReceiversData.Empty + return ReceiversData(listOf(ExpressionReceiver(receiverExpression, expressionType)), receiverData.second) } else { - val resolutionScope = context[BindingContext.RESOLUTION_SCOPE, expression] ?: return listOf() - return resolutionScope.getImplicitReceiversHierarchy().map { it.getValue() } + val resolutionScope = context[BindingContext.RESOLUTION_SCOPE, expression] ?: return ReceiversData.Empty + return ReceiversData(resolutionScope.getImplicitReceiversHierarchy().map { it.getValue() }, CallType.NORMAL) } } @@ -141,15 +158,33 @@ public class ReferenceVariantsHelper( return type } - private fun getReferenceVariantsReceiver(expression: JetSimpleNameExpression): JetExpression? { + private fun getReferenceVariantsReceiver(expression: JetSimpleNameExpression): Pair? { val parent = expression.getParent() val inPositionForCompletionWithReceiver = parent is JetCallExpression || parent is JetQualifiedExpression || parent is JetBinaryExpression - return if (inPositionForCompletionWithReceiver) - expression.getReceiverExpression() - else - null + if (!inPositionForCompletionWithReceiver) return null + val receiverExpression = expression.getReceiverExpression() ?: return null + val callType = when (parent) { + is JetBinaryExpression -> CallType.INFIX + + is JetCallExpression -> { + if ((parent.getParent() as JetQualifiedExpression).getOperationSign() == JetTokens.SAFE_ACCESS) + CallType.SAFE + else + CallType.NORMAL + } + + is JetQualifiedExpression -> { + if (parent.getOperationSign() == JetTokens.SAFE_ACCESS) + CallType.SAFE + else + CallType.NORMAL + } + + else -> error("Unknown parent") + } + return receiverExpression to callType } private fun MutableCollection.addCallableExtensions( diff --git a/idea/src/org/jetbrains/jet/plugin/completion/CompletionSession.kt b/idea/src/org/jetbrains/jet/plugin/completion/CompletionSession.kt index 3e0e69235d9..414e2cc35de 100644 --- a/idea/src/org/jetbrains/jet/plugin/completion/CompletionSession.kt +++ b/idea/src/org/jetbrains/jet/plugin/completion/CompletionSession.kt @@ -40,6 +40,7 @@ import org.jetbrains.jet.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.jet.plugin.refactoring.comparePossiblyOverridingDescriptors import kotlin.properties.Delegates import org.jetbrains.jet.lang.psi.psiUtil.getStrictParentOfType +import org.jetbrains.jet.plugin.util.makeNotNullable class CompletionSessionConfiguration( val completeNonImportedDeclarations: Boolean, @@ -74,11 +75,14 @@ abstract class CompletionSessionBase(protected val configuration: CompletionSess protected val boldImmediateLookupElementFactory: LookupElementFactory = run { if (jetReference != null) { val expression = jetReference.expression - val receivers = referenceVariantsHelper!!.getReferenceVariantsReceivers(expression) + val (receivers, callType) = referenceVariantsHelper!!.getReferenceVariantsReceivers(expression) val dataFlowInfo = bindingContext!!.getDataFlowInfo(expression) - val receiverTypes = receivers.flatMap { + var receiverTypes = receivers.flatMap { SmartCastUtils.getSmartCastVariantsWithLessSpecificExcluded(it, bindingContext, dataFlowInfo) } + if (callType == ReferenceVariantsHelper.CallType.SAFE) { + receiverTypes = receiverTypes.map { it.makeNotNullable() } + } BoldImmediateLookupElementFactory(receiverTypes) } else { diff --git a/idea/src/org/jetbrains/jet/plugin/completion/LookupElementFactory.kt b/idea/src/org/jetbrains/jet/plugin/completion/LookupElementFactory.kt index 3fbb83f19ef..376809ecd1a 100644 --- a/idea/src/org/jetbrains/jet/plugin/completion/LookupElementFactory.kt +++ b/idea/src/org/jetbrains/jet/plugin/completion/LookupElementFactory.kt @@ -36,6 +36,7 @@ import com.intellij.codeInsight.lookup.LookupElementDecorator import com.intellij.codeInsight.lookup.LookupElementPresentation import org.jetbrains.jet.lang.types.JetType import org.jetbrains.jet.plugin.caches.resolve.ResolutionFacade +import java.awt.Color public open class LookupElementFactory protected() { public open fun createLookupElement(resolutionFacade: ResolutionFacade, descriptor: DeclarationDescriptor): LookupElement { @@ -162,19 +163,44 @@ public class BoldImmediateLookupElementFactory(private val receiverTypes: Collec val element = super.createLookupElement(resolutionFacade, descriptor) if (descriptor !is CallableMemberDescriptor) return element + + val isReceiverNullable = receiverTypes.all { it.isNullable() } val receiverParameter = descriptor.getExtensionReceiverParameter() - val bold = if (receiverParameter != null) { - receiverTypes.any { it == receiverParameter.getType() } + + val style: Style = if (receiverParameter != null) { + val receiverParamType = receiverParameter.getType() + if (isReceiverNullable && !receiverParamType.isNullable()) + Style.GRAYED + else if (receiverTypes.any { it == receiverParamType }) + Style.BOLD + else + Style.NORMAL } else { - descriptor.getContainingDeclaration() is ClassifierDescriptor && descriptor.getKind() == CallableMemberDescriptor.Kind.DECLARATION + if (isReceiverNullable) + Style.GRAYED + else if (descriptor.getContainingDeclaration() is ClassifierDescriptor && descriptor.getKind() == CallableMemberDescriptor.Kind.DECLARATION) + Style.BOLD + else + Style.NORMAL } - return if (bold) { + return if (style != Style.NORMAL) { object : LookupElementDecorator(element) { override fun renderElement(presentation: LookupElementPresentation) { super.renderElement(presentation) - presentation.setItemTextBold(true) + if (style == Style.BOLD) { + presentation.setItemTextBold(true) + } + else { + presentation.setItemTextForeground(Color.GRAY) + // gray all tail fragments too: + val fragments = presentation.getTailFragments() + presentation.clearTail() + for (fragment in fragments) { + presentation.appendTailText(fragment.text, true) + } + } } } } @@ -182,4 +208,10 @@ public class BoldImmediateLookupElementFactory(private val receiverTypes: Collec element } } + + private enum class Style { + NORMAL + BOLD + GRAYED + } } diff --git a/idea/testData/completion/basic/common/AfterNullable.kt b/idea/testData/completion/basic/common/AfterNullable.kt new file mode 100644 index 00000000000..3bdee2a76a5 --- /dev/null +++ b/idea/testData/completion/basic/common/AfterNullable.kt @@ -0,0 +1,14 @@ +fun String?.forNullableString(){} +fun Any?.forNullableAny(){} +fun String.forString(){} +fun Any.forAny(){} + +fun foo(s: String?) { + s. +} + +// EXIST: { lookupString: "forNullableString", attributes: "bold" } +// EXIST: { lookupString: "forNullableAny", attributes: "" } +// EXIST: { lookupString: "forString", attributes: "grayed" } +// EXIST: { lookupString: "forAny", attributes: "grayed" } +// EXIST: { lookupString: "compareTo", attributes: "grayed" } diff --git a/idea/testData/completion/basic/common/AfterNullableAutoCast.kt b/idea/testData/completion/basic/common/AfterNullableAutoCast.kt new file mode 100644 index 00000000000..ac64bb9b10f --- /dev/null +++ b/idea/testData/completion/basic/common/AfterNullableAutoCast.kt @@ -0,0 +1,16 @@ +fun String?.forNullableString(){} +fun Any?.forNullableAny(){} +fun String.forString(){} +fun Any.forAny(){} + +fun foo(o: Any?) { + if (o is String) { + o. + } +} + +// EXIST: { lookupString: "forNullableString", attributes: "" } +// EXIST: { lookupString: "forNullableAny", attributes: "" } +// EXIST: { lookupString: "forString", attributes: "bold" } +// EXIST: { lookupString: "forAny", attributes: "" } +// EXIST: { lookupString: "compareTo", attributes: "bold" } diff --git a/idea/testData/completion/basic/common/AfterNullableAutoCast2.kt b/idea/testData/completion/basic/common/AfterNullableAutoCast2.kt new file mode 100644 index 00000000000..ab2e98c99be --- /dev/null +++ b/idea/testData/completion/basic/common/AfterNullableAutoCast2.kt @@ -0,0 +1,30 @@ +trait T1 { + fun inT1(){} +} + +trait T2 { + fun inT2(){} +} + +fun T1.forT1(){} +fun T2.forT2(){} +fun T1?.forNullableT1(){} +fun T2?.forNullableT2(){} +fun Any.forAny(){} +fun Any?.forNullableAny(){} + +fun foo(o: T1?) { + if (o is T2) { + o. + } +} + +// EXIST: { lookupString: "inT1", attributes: "bold" } +// EXIST: { lookupString: "inT2", attributes: "bold" } +// EXIST: { lookupString: "hashCode", attributes: "" } +// EXIST: { lookupString: "forT1", attributes: "bold" } +// EXIST: { lookupString: "forT2", attributes: "bold" } +// EXIST: { lookupString: "forNullableT1", attributes: "" } +// EXIST: { lookupString: "forNullableT2", attributes: "" } +// EXIST: { lookupString: "forAny", attributes: "" } +// EXIST: { lookupString: "forNullableAny", attributes: "" } diff --git a/idea/testData/completion/basic/common/SafeCallAfterNullable.kt b/idea/testData/completion/basic/common/SafeCallAfterNullable.kt new file mode 100644 index 00000000000..1e4b1b89792 --- /dev/null +++ b/idea/testData/completion/basic/common/SafeCallAfterNullable.kt @@ -0,0 +1,14 @@ +fun String?.forNullableString(){} +fun Any?.forNullableAny(){} +fun String.forString(){} +fun Any.forAny(){} + +fun foo(s: String?) { + s?. +} + +// EXIST: { lookupString: "forNullableString", attributes: "" } +// EXIST: { lookupString: "forNullableAny", attributes: "" } +// EXIST: { lookupString: "forString", attributes: "bold" } +// EXIST: { lookupString: "forAny", attributes: "" } +// EXIST: { lookupString: "compareTo", attributes: "bold" } diff --git a/idea/tests/org/jetbrains/jet/completion/ExpectedCompletionUtils.java b/idea/tests/org/jetbrains/jet/completion/ExpectedCompletionUtils.java index f87f408cdd3..7c60fe78136 100644 --- a/idea/tests/org/jetbrains/jet/completion/ExpectedCompletionUtils.java +++ b/idea/tests/org/jetbrains/jet/completion/ExpectedCompletionUtils.java @@ -26,6 +26,7 @@ import com.google.gson.JsonParser; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.JBColor; import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,7 +35,9 @@ import org.jetbrains.jet.plugin.project.TargetPlatform; import org.jetbrains.jet.plugin.stubs.AstAccessControl; import org.junit.Assert; +import java.awt.*; import java.util.*; +import java.util.List; /** * Extract a number of statements about completion from the given text. Those statements @@ -321,6 +324,12 @@ public class ExpectedCompletionUtils { if (builder.length() > 0) builder.append(" "); builder.append("underlined"); } + Color foreground = presentation.getItemTextForeground(); + if (!foreground.equals(JBColor.foreground())) { + assert foreground.equals(Color.GRAY); + if (builder.length() > 0) builder.append(" "); + builder.append("grayed"); + } if (presentation.isStrikeout()) { if (builder.length() > 0) builder.append(" "); builder.append("strikeout"); diff --git a/idea/tests/org/jetbrains/jet/completion/JSBasicCompletionTestGenerated.java b/idea/tests/org/jetbrains/jet/completion/JSBasicCompletionTestGenerated.java index 29e05bd0e5c..9e5fe380523 100644 --- a/idea/tests/org/jetbrains/jet/completion/JSBasicCompletionTestGenerated.java +++ b/idea/tests/org/jetbrains/jet/completion/JSBasicCompletionTestGenerated.java @@ -48,6 +48,24 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes doTest(fileName); } + @TestMetadata("AfterNullable.kt") + public void testAfterNullable() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/AfterNullable.kt"); + doTest(fileName); + } + + @TestMetadata("AfterNullableAutoCast.kt") + public void testAfterNullableAutoCast() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/AfterNullableAutoCast.kt"); + doTest(fileName); + } + + @TestMetadata("AfterNullableAutoCast2.kt") + public void testAfterNullableAutoCast2() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/AfterNullableAutoCast2.kt"); + doTest(fileName); + } + public void testAllFilesPresentInCommon() throws Exception { JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/completion/basic/common"), Pattern.compile("^(.+)\\.kt$"), true); } @@ -730,6 +748,12 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes doTest(fileName); } + @TestMetadata("SafeCallAfterNullable.kt") + public void testSafeCallAfterNullable() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/SafeCallAfterNullable.kt"); + doTest(fileName); + } + @TestMetadata("ShortClassNamesInTypePosition.kt") public void testShortClassNamesInTypePosition() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/ShortClassNamesInTypePosition.kt"); diff --git a/idea/tests/org/jetbrains/jet/completion/JvmBasicCompletionTestGenerated.java b/idea/tests/org/jetbrains/jet/completion/JvmBasicCompletionTestGenerated.java index e85aa08848a..cae5c8433b3 100644 --- a/idea/tests/org/jetbrains/jet/completion/JvmBasicCompletionTestGenerated.java +++ b/idea/tests/org/jetbrains/jet/completion/JvmBasicCompletionTestGenerated.java @@ -48,6 +48,24 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT doTest(fileName); } + @TestMetadata("AfterNullable.kt") + public void testAfterNullable() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/AfterNullable.kt"); + doTest(fileName); + } + + @TestMetadata("AfterNullableAutoCast.kt") + public void testAfterNullableAutoCast() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/AfterNullableAutoCast.kt"); + doTest(fileName); + } + + @TestMetadata("AfterNullableAutoCast2.kt") + public void testAfterNullableAutoCast2() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/AfterNullableAutoCast2.kt"); + doTest(fileName); + } + public void testAllFilesPresentInCommon() throws Exception { JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/completion/basic/common"), Pattern.compile("^(.+)\\.kt$"), true); } @@ -730,6 +748,12 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT doTest(fileName); } + @TestMetadata("SafeCallAfterNullable.kt") + public void testSafeCallAfterNullable() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/SafeCallAfterNullable.kt"); + doTest(fileName); + } + @TestMetadata("ShortClassNamesInTypePosition.kt") public void testShortClassNamesInTypePosition() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/ShortClassNamesInTypePosition.kt");