From 84e530bc1a82bb2fa7dfdfef9b85835c6e94fbb4 Mon Sep 17 00:00:00 2001 From: Valentin Kipyatkov Date: Wed, 20 Jan 2016 13:12:44 +0300 Subject: [PATCH] KT-6079 Show "parameter information" popup for type arguments #KT-6079 Fixed --- .../org/jetbrains/kotlin/types/TypeUtils.kt | 1 + idea/src/META-INF/plugin.xml | 2 + .../idea/editor/KotlinTypedHandler.java | 1 + .../KotlinTypeArgumentInfoHandler.kt | 217 ++++++++++++++++++ .../parameterInfo/typeArguments/BaseClass.kt | 4 + .../typeArguments/Constraints.kt | 5 + .../typeArguments/ConstructorCall.kt | 5 + .../typeArguments/FunctionCall.kt | 7 + .../parameterInfo/typeArguments/JavaClass.kt | 7 + .../parameterInfo/typeArguments/Overloads.kt | 12 + .../parameterInfo/typeArguments/Reified.kt | 7 + .../typeArguments/VariableType.kt | 5 + .../AbstractParameterInfoTest.kt | 4 +- .../ParameterInfoTestGenerated.java | 57 +++++ 14 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/parameterInfo/KotlinTypeArgumentInfoHandler.kt create mode 100644 idea/testData/parameterInfo/typeArguments/BaseClass.kt create mode 100644 idea/testData/parameterInfo/typeArguments/Constraints.kt create mode 100644 idea/testData/parameterInfo/typeArguments/ConstructorCall.kt create mode 100644 idea/testData/parameterInfo/typeArguments/FunctionCall.kt create mode 100644 idea/testData/parameterInfo/typeArguments/JavaClass.kt create mode 100644 idea/testData/parameterInfo/typeArguments/Overloads.kt create mode 100644 idea/testData/parameterInfo/typeArguments/Reified.kt create mode 100644 idea/testData/parameterInfo/typeArguments/VariableType.kt diff --git a/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt b/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt index 9bedc061bfa..f617185be69 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt +++ b/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt @@ -52,6 +52,7 @@ fun KotlinType.isNothing(): Boolean = KotlinBuiltIns.isNothing(this) fun KotlinType.isNullableNothing(): Boolean = KotlinBuiltIns.isNullableNothing(this) fun KotlinType.isUnit(): Boolean = KotlinBuiltIns.isUnit(this) fun KotlinType.isAnyOrNullableAny(): Boolean = KotlinBuiltIns.isAnyOrNullableAny(this) +fun KotlinType.isNullableAny(): Boolean = KotlinBuiltIns.isNullableAny(this) fun KotlinType.isBoolean(): Boolean = KotlinBuiltIns.isBoolean(this) fun KotlinType.isBooleanOrNullableBoolean(): Boolean = KotlinBuiltIns.isBooleanOrNullableBoolean(this) diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 7d31a9851ae..481e08c7fb0 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -364,6 +364,8 @@ + + diff --git a/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java b/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java index 4449a093eae..a1f962cb7e4 100644 --- a/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java +++ b/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java @@ -75,6 +75,7 @@ public class KotlinTypedHandler extends TypedHandlerDelegate { case '<': kotlinLTTyped = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET && LtGtTypingUtils.shouldAutoCloseAngleBracket(editor.getCaretModel().getOffset(), editor); + autoPopupParameterInfo(project, editor); return Result.CONTINUE; case '>': diff --git a/idea/src/org/jetbrains/kotlin/idea/parameterInfo/KotlinTypeArgumentInfoHandler.kt b/idea/src/org/jetbrains/kotlin/idea/parameterInfo/KotlinTypeArgumentInfoHandler.kt new file mode 100644 index 00000000000..e11cedc53dc --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/parameterInfo/KotlinTypeArgumentInfoHandler.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.parameterInfo + +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.lang.parameterInfo.* +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.core.resolveCandidates +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.allChildren +import org.jetbrains.kotlin.psi.psiUtil.getParentOfType +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.renderer.DescriptorRenderer +import org.jetbrains.kotlin.resolve.calls.callUtil.getCall +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.Variance +import org.jetbrains.kotlin.types.typeUtil.TypeNullability +import org.jetbrains.kotlin.types.typeUtil.isAnyOrNullableAny +import org.jetbrains.kotlin.types.typeUtil.nullability + +class KotlinClassTypeArgumentInfoHandler : KotlinTypeArgumentInfoHandlerBase() { + override fun fetchTypeParameters(descriptor: ClassDescriptor) = descriptor.typeConstructor.parameters + + override fun findParameterOwners(argumentList: KtTypeArgumentList): Collection? { + val userType = argumentList.parent as? KtUserType ?: return null + val bindingContext = argumentList.analyze(BodyResolveMode.PARTIAL) + return userType.referenceExpression?.mainReference?.resolveToDescriptors(bindingContext)?.mapNotNull { it as? ClassDescriptor } + } + + override fun getArgumentListAllowedParentClasses() = setOf(KtUserType::class.java) +} + +class KotlinFunctionTypeArgumentInfoHandler : KotlinTypeArgumentInfoHandlerBase() { + override fun fetchTypeParameters(descriptor: FunctionDescriptor) = descriptor.typeParameters + + override fun findParameterOwners(argumentList: KtTypeArgumentList): Collection? { + val callElement = argumentList.parent as? KtCallElement ?: return null + val bindingContext = argumentList.analyze(BodyResolveMode.PARTIAL) + val call = callElement.getCall(bindingContext) ?: return null + val candidates = call.resolveCandidates(bindingContext, callElement.getResolutionFacade()) + return candidates + .map { it.resultingDescriptor } + .distinctBy { buildPresentation(it.typeParameters, -1).first } + } + + override fun getArgumentListAllowedParentClasses() = setOf(KtCallElement::class.java) +} + +abstract class KotlinTypeArgumentInfoHandlerBase : ParameterInfoHandlerWithTabActionSupport { + protected abstract fun fetchTypeParameters(descriptor: TParameterOwner): List + protected abstract fun findParameterOwners(argumentList: KtTypeArgumentList): Collection? + + override fun getActualParameterDelimiterType() = KtTokens.COMMA + override fun getActualParametersRBraceType() = KtTokens.GT + + override fun getArgumentListClass() = KtTypeArgumentList::class.java + + override fun getActualParameters(o: KtTypeArgumentList) = o.arguments.toTypedArray() + + override fun getArgListStopSearchClasses() = setOf(KtNamedFunction::class.java, KtVariableDeclaration::class.java, KtClassOrObject::class.java) + + override fun getParameterCloseChars() = ParameterInfoUtils.DEFAULT_PARAMETER_CLOSE_CHARS + + override fun tracksParameterIndex() = true + + override fun couldShowInLookup() = false + override fun getParametersForLookup(item: LookupElement?, context: ParameterInfoContext?) = emptyArray() + override fun getParametersForDocumentation(p: TParameterOwner, context: ParameterInfoContext?) = emptyArray() + + override fun showParameterInfo(element: KtTypeArgumentList, context: CreateParameterInfoContext) { + context.showHint(element, element.textRange.startOffset, this) + } + + override fun findElementForUpdatingParameterInfo(context: UpdateParameterInfoContext): KtTypeArgumentList? { + val element = context.file.findElementAt(context.offset) ?: return null + val argumentList = element.getParentOfType(true) ?: return null + val argument = element.parents.takeWhile { it != argumentList }.lastOrNull() as? KtTypeProjection + if (argument != null) { + val arguments = getActualParameters(argumentList) + val index = arguments.indexOf(element) + context.setCurrentParameter(index) + context.setHighlightedParameter(element) + } + return argumentList + } + + override fun findElementForParameterInfo(context: CreateParameterInfoContext): KtTypeArgumentList? { + val file = context.file as? KtFile ?: return null + + val token = file.findElementAt(context.offset) ?: return null + val argumentList = token.getParentOfType(true) ?: return null + + val parameterOwners = findParameterOwners(argumentList) ?: return null + + context.itemsToShow = parameterOwners.toTypedArray() + return argumentList + } + + override fun updateParameterInfo(argumentList: KtTypeArgumentList, context: UpdateParameterInfoContext) { + if (context.parameterOwner !== argumentList) { + context.removeHint() + } + + val offset = context.offset + val parameterIndex = argumentList.allChildren + .takeWhile { it.startOffset < offset } + .count { it.node.elementType == KtTokens.COMMA } + context.setCurrentParameter(parameterIndex) + } + + override fun updateUI(itemToShow: TParameterOwner, context: ParameterInfoUIContext) { + if (!updateUIOrFail(itemToShow, context)) { + context.isUIComponentEnabled = false + return + } + } + + private fun updateUIOrFail(itemToShow: TParameterOwner, context: ParameterInfoUIContext): Boolean { + val currentIndex = context.currentParameterIndex + if (currentIndex < 0) return false // by some strange reason we are invoked with currentParameterIndex == -1 during initialization + + val parameters = fetchTypeParameters(itemToShow) + + val (text, currentParameterStart, currentParameterEnd) = buildPresentation(parameters, currentIndex) + + context.setupUIComponentPresentation(text, currentParameterStart, currentParameterEnd, + false/*isDisabled*/, false/*strikeout*/, false/*isDisabledBeforeHighlight*/, + context.defaultParameterColor) + + return true + } + + protected fun buildPresentation( + parameters: List, + currentIndex: Int + ): Triple { + var currentParameterStart = -1 + var currentParameterEnd = -1 + + val text = buildString { + var needWhere = false + for ((index, parameter) in parameters.withIndex()) { + if (index > 0) append(", ") + + if (index == currentIndex) { + currentParameterStart = length + } + + if (parameter.isReified) { + append("reified ") + } + + when (parameter.variance) { + Variance.INVARIANT -> {} + Variance.IN_VARIANCE -> append("in ") + Variance.OUT_VARIANCE -> append("out ") + } + + append(parameter.name.asString()) + + val upperBounds = parameter.upperBounds + if (upperBounds.size == 1) { + val upperBound = upperBounds.single() + if (!upperBound.isAnyOrNullableAny() || upperBound.nullability() == TypeNullability.NOT_NULL) { // skip Any? or Any! + append(" : ").append(DescriptorRenderer.SHORT_NAMES_IN_TYPES.renderType(upperBound)) + } + } + else if (upperBounds.size > 1) { + needWhere = true + } + + if (index == currentIndex) { + currentParameterEnd = length + } + } + + if (needWhere) { + append(" where ") + + var needComma = false + for (parameter in parameters) { + val upperBounds = parameter.upperBounds + if (upperBounds.size > 1) { + for (upperBound in upperBounds) { + if (needComma) append(", ") + needComma = true + append(parameter.name.asString()).append(" : ").append(DescriptorRenderer.SHORT_NAMES_IN_TYPES.renderType(upperBound)) + } + } + } + } + } + return Triple(text, currentParameterStart, currentParameterEnd) + } +} \ No newline at end of file diff --git a/idea/testData/parameterInfo/typeArguments/BaseClass.kt b/idea/testData/parameterInfo/typeArguments/BaseClass.kt new file mode 100644 index 00000000000..86d4a801107 --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/BaseClass.kt @@ -0,0 +1,4 @@ +open class Base + +class X : Base<> +//Text: (out T, in V), Disabled: false, Strikeout: false, Green: false \ No newline at end of file diff --git a/idea/testData/parameterInfo/typeArguments/Constraints.kt b/idea/testData/parameterInfo/typeArguments/Constraints.kt new file mode 100644 index 00000000000..3cb9589297f --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/Constraints.kt @@ -0,0 +1,5 @@ +open class A> where T2 : Runnable, T2 : Cloneable, T3 : Runnable + +val v: A> + +//Text: (T1 : Any, T2, T3 where T2 : Runnable, T2 : Cloneable, T3 : List, T3 : Runnable), Disabled: false, Strikeout: false, Green: false diff --git a/idea/testData/parameterInfo/typeArguments/ConstructorCall.kt b/idea/testData/parameterInfo/typeArguments/ConstructorCall.kt new file mode 100644 index 00000000000..ff36dadba0e --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/ConstructorCall.kt @@ -0,0 +1,5 @@ +open class Base + +val v = Base<> + +//Text: (out T, in V), Disabled: false, Strikeout: false, Green: false \ No newline at end of file diff --git a/idea/testData/parameterInfo/typeArguments/FunctionCall.kt b/idea/testData/parameterInfo/typeArguments/FunctionCall.kt new file mode 100644 index 00000000000..a175b300b68 --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/FunctionCall.kt @@ -0,0 +1,7 @@ +fun foo(t: T): T = t + +fun bar() { + foo<>() +} + +//Text: (T, V), Disabled: false, Strikeout: false, Green: false diff --git a/idea/testData/parameterInfo/typeArguments/JavaClass.kt b/idea/testData/parameterInfo/typeArguments/JavaClass.kt new file mode 100644 index 00000000000..dd3293cd377 --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/JavaClass.kt @@ -0,0 +1,7 @@ +import java.util.* + +fun foo() { + ArrayList<> +} + +//Text: (E), Disabled: false, Strikeout: false, Green: false \ No newline at end of file diff --git a/idea/testData/parameterInfo/typeArguments/Overloads.kt b/idea/testData/parameterInfo/typeArguments/Overloads.kt new file mode 100644 index 00000000000..c6bf7ecbd53 --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/Overloads.kt @@ -0,0 +1,12 @@ +fun foo(t: T): T = t +fun foo(t: T, p: Int): T = t +fun foo(p: Boolean, x: X) {} + +fun bar() { + foo<>() +} + +/* +Text: (T, V), Disabled: false, Strikeout: false, Green: false +Text: (X), Disabled: false, Strikeout: false, Green: false +*/ \ No newline at end of file diff --git a/idea/testData/parameterInfo/typeArguments/Reified.kt b/idea/testData/parameterInfo/typeArguments/Reified.kt new file mode 100644 index 00000000000..e38f280ecf9 --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/Reified.kt @@ -0,0 +1,7 @@ +inline fun foo(p: T) {} + +fun bar() { + foo<> +} + +//Text: (reified T), Disabled: false, Strikeout: false, Green: false diff --git a/idea/testData/parameterInfo/typeArguments/VariableType.kt b/idea/testData/parameterInfo/typeArguments/VariableType.kt new file mode 100644 index 00000000000..20ea0f443ec --- /dev/null +++ b/idea/testData/parameterInfo/typeArguments/VariableType.kt @@ -0,0 +1,5 @@ +open class A + +val v: A> + +//Text: (T, V), Disabled: false, Strikeout: false, Green: false diff --git a/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/AbstractParameterInfoTest.kt b/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/AbstractParameterInfoTest.kt index be2bff367f2..0a91b5b23d4 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/AbstractParameterInfoTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/AbstractParameterInfoTest.kt @@ -22,7 +22,6 @@ import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase -import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.test.PluginTestCaseBase import org.jetbrains.kotlin.idea.test.ProjectDescriptorWithStdlibSources @@ -77,8 +76,7 @@ abstract class AbstractParameterInfoTest : LightCodeInsightFixtureTestCase() { val parameterInfoUIContext = MockParameterInfoUIContext(parameterOwner, updateContext.currentParameter) for (item in mockCreateParameterInfoContext.itemsToShow) { - //noinspection unchecked - handler.updateUI(item as FunctionDescriptor, parameterInfoUIContext) + handler.updateUI(item, parameterInfoUIContext) } Assert.assertEquals(expectedResultText, parameterInfoUIContext.resultText) } diff --git a/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/ParameterInfoTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/ParameterInfoTestGenerated.java index ac587e516af..7cd934fb0f7 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/ParameterInfoTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/parameterInfo/ParameterInfoTestGenerated.java @@ -304,4 +304,61 @@ public class ParameterInfoTestGenerated extends AbstractParameterInfoTest { doTest(fileName); } } + + @TestMetadata("idea/testData/parameterInfo/typeArguments") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class TypeArguments extends AbstractParameterInfoTest { + public void testAllFilesPresentInTypeArguments() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/parameterInfo/typeArguments"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("BaseClass.kt") + public void testBaseClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/BaseClass.kt"); + doTest(fileName); + } + + @TestMetadata("Constraints.kt") + public void testConstraints() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/Constraints.kt"); + doTest(fileName); + } + + @TestMetadata("ConstructorCall.kt") + public void testConstructorCall() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/ConstructorCall.kt"); + doTest(fileName); + } + + @TestMetadata("FunctionCall.kt") + public void testFunctionCall() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/FunctionCall.kt"); + doTest(fileName); + } + + @TestMetadata("JavaClass.kt") + public void testJavaClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/JavaClass.kt"); + doTest(fileName); + } + + @TestMetadata("Overloads.kt") + public void testOverloads() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/Overloads.kt"); + doTest(fileName); + } + + @TestMetadata("Reified.kt") + public void testReified() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/Reified.kt"); + doTest(fileName); + } + + @TestMetadata("VariableType.kt") + public void testVariableType() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/parameterInfo/typeArguments/VariableType.kt"); + doTest(fileName); + } + } }