Code completion: graying members after dot completion for nullable value

This commit is contained in:
Valentin Kipyatkov
2014-11-28 17:48:30 +03:00
parent a60f32b919
commit acfd308a7e
10 changed files with 226 additions and 24 deletions
@@ -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<ReceiverValue>,
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<DeclarationDescriptor>()
@@ -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<ReceiverValue> {
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<JetExpression, CallType>? {
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<DeclarationDescriptor>.addCallableExtensions(
@@ -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 {
@@ -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<LookupElement>(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
}
}
@@ -0,0 +1,14 @@
fun String?.forNullableString(){}
fun Any?.forNullableAny(){}
fun String.forString(){}
fun Any.forAny(){}
fun foo(s: String?) {
s.<caret>
}
// EXIST: { lookupString: "forNullableString", attributes: "bold" }
// EXIST: { lookupString: "forNullableAny", attributes: "" }
// EXIST: { lookupString: "forString", attributes: "grayed" }
// EXIST: { lookupString: "forAny", attributes: "grayed" }
// EXIST: { lookupString: "compareTo", attributes: "grayed" }
@@ -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.<caret>
}
}
// EXIST: { lookupString: "forNullableString", attributes: "" }
// EXIST: { lookupString: "forNullableAny", attributes: "" }
// EXIST: { lookupString: "forString", attributes: "bold" }
// EXIST: { lookupString: "forAny", attributes: "" }
// EXIST: { lookupString: "compareTo", attributes: "bold" }
@@ -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.<caret>
}
}
// 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: "" }
@@ -0,0 +1,14 @@
fun String?.forNullableString(){}
fun Any?.forNullableAny(){}
fun String.forString(){}
fun Any.forAny(){}
fun foo(s: String?) {
s?.<caret>
}
// EXIST: { lookupString: "forNullableString", attributes: "" }
// EXIST: { lookupString: "forNullableAny", attributes: "" }
// EXIST: { lookupString: "forString", attributes: "bold" }
// EXIST: { lookupString: "forAny", attributes: "" }
// EXIST: { lookupString: "compareTo", attributes: "bold" }
@@ -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");
@@ -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");
@@ -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");