[groovy] parameter hints (IDEA-163484)
authorDaniil Ovchinnikov <daniil.ovchinnikov@jetbrains.com>
Wed, 2 Nov 2016 20:07:41 +0000 (23:07 +0300)
committerDaniil Ovchinnikov <daniil.ovchinnikov@jetbrains.com>
Thu, 3 Nov 2016 14:01:08 +0000 (17:01 +0300)
plugins/groovy/src/META-INF/plugin.xml
plugins/groovy/src/org/jetbrains/plugins/groovy/codeInsight/hint/GroovyInlayParameterHintsProvider.kt [new file with mode: 0644]
plugins/groovy/test/org/jetbrains/plugins/groovy/codeInsight/hint/GroovyInlayParameterHintsProviderTest.groovy [new file with mode: 0644]

index 329f5a0327f672d6b3e0f17a48f9c292f11c6653..6fb53033ed93a6c1aff10d88af0ef816d63556b0 100644 (file)
     <codeInsight.createFieldFromUsageHelper language="Groovy"
                                             implementationClass="org.jetbrains.plugins.groovy.annotator.intentions.GroovyCreateFieldFromUsageHelper"/>
     <codeInsight.typeInfo language="Groovy" implementationClass="org.jetbrains.plugins.groovy.codeInsight.hint.GroovyExpressionTypeProvider"/>
+    <codeInsight.parameterNameHints
+        language="Groovy" implementationClass="org.jetbrains.plugins.groovy.codeInsight.hint.GroovyInlayParameterHintsProvider"
+    />
 
     <fileTypeFactory implementation="org.jetbrains.plugins.groovy.GroovyFileTypeLoader"/>
     <fileTypeFactory implementation="org.jetbrains.plugins.groovy.dgm.DGMFileTypeFactory"/>
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/codeInsight/hint/GroovyInlayParameterHintsProvider.kt b/plugins/groovy/src/org/jetbrains/plugins/groovy/codeInsight/hint/GroovyInlayParameterHintsProvider.kt
new file mode 100644 (file)
index 0000000..d6ebc52
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-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.plugins.groovy.codeInsight.hint
+
+import com.intellij.codeInsight.hints.InlayInfo
+import com.intellij.codeInsight.hints.InlayParameterHintsProvider
+import com.intellij.codeInsight.hints.MethodInfo
+import com.intellij.lang.java.JavaLanguage
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrGdkMethod
+import org.jetbrains.plugins.groovy.lang.psi.impl.signatures.GrClosureSignatureUtil
+
+class GroovyInlayParameterHintsProvider : InlayParameterHintsProvider {
+
+  private companion object {
+    val blackList = setOf(
+        "org.codehaus.groovy.runtime.DefaultGroovyMethods.*"
+    )
+  }
+
+  override fun getParameterHints(element: PsiElement) = (element as? GrCall)?.doGetParameterHints() ?: emptyList()
+
+  private fun GrCall.doGetParameterHints(): List<InlayInfo>? {
+    val signature = GrClosureSignatureUtil.createSignature(this) ?: return null
+    val infos = GrClosureSignatureUtil.mapParametersToArguments(signature, this) ?: return null
+    val original = signature.parameters.zip(infos)
+
+    // leave only parameters with names
+    val map = original.mapNotNull {
+      it.first.name?.let { name -> name to it.second }
+    }.toMap()
+
+    // leave only regular arguments and varargs
+    val nonNamedArguments = map.filterValues {
+      !it.isMultiArg || it.args.none { it is GrNamedArgument }
+    }
+
+    return nonNamedArguments.mapNotNull {
+      val (name, info) = it
+      info.args.firstOrNull()?.let { arg ->
+        val inlayText = if (info.isMultiArg) "...$name" else name
+        InlayInfo(inlayText, arg.textRange.startOffset)
+      }
+    }
+  }
+
+  override fun getMethodInfo(element: PsiElement): MethodInfo? {
+    val call = element as? GrCall
+    val resolved = call?.resolveMethod()
+    val method = (resolved as? GrGdkMethod)?.staticMethod ?: resolved
+    return method?.getMethodInfo()
+  }
+
+  private fun PsiMethod.getMethodInfo(): MethodInfo? {
+    val clazzName = containingClass?.qualifiedName ?: return null
+    val fullMethodName = StringUtil.getQualifiedName(clazzName, name)
+    val paramNames: List<String> = parameterList.parameters.map { it.name ?: "" }
+    return MethodInfo(fullMethodName, paramNames)
+  }
+
+  override val defaultBlackList: Set<String> get() = blackList
+
+  override fun getBlackListDependencyLanguage() = JavaLanguage.INSTANCE
+}
\ No newline at end of file
diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/codeInsight/hint/GroovyInlayParameterHintsProviderTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/codeInsight/hint/GroovyInlayParameterHintsProviderTest.groovy
new file mode 100644 (file)
index 0000000..acafacf
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2000-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.plugins.groovy.codeInsight.hint
+
+import com.intellij.codeInsight.daemon.impl.ParameterHintsPresentationManager
+import com.intellij.testFramework.LightProjectDescriptor
+import groovy.transform.CompileStatic
+import org.jetbrains.plugins.groovy.GroovyLightProjectDescriptor
+import org.jetbrains.plugins.groovy.LightGroovyTestCase
+
+@CompileStatic
+class GroovyInlayParameterHintsProviderTest extends LightGroovyTestCase {
+
+  final LightProjectDescriptor projectDescriptor = GroovyLightProjectDescriptor.GROOVY_LATEST
+  String prefix
+
+  @Override
+  void setUp() {
+    super.setUp()
+    fixture.addFileToProject 'Foo.groovy', '''\
+class Foo {
+  Foo() {}
+  Foo(a, b, c) {}
+  void simple(a) {}
+  void defaultArgs(a, b = 1,c) {}
+  void mapArgs(Map m, a) {}
+  void varArgs(a, def... b) {}
+  void combo(Map m, a, b = 1, def ... c) {}
+  def getAt(a) {}
+}
+'''
+  }
+
+  private void testInlays(String text, Map<Integer, String> expectedParameterHints) {
+    fixture.configureByText '_.groovy', prefix ? prefix + text : text
+    fixture.doHighlighting()
+    def manager = ParameterHintsPresentationManager.instance
+    def inlays = editor.inlayModel.getInlineElementsInRange(0, editor.document.textLength)
+    def parameterHinInlays = inlays.findAll {
+      manager.isParameterHint(it)
+    }
+    int prefixLength = prefix?.length() ?: 0
+    def actualParameterHints = parameterHinInlays.collectEntries {
+      [(it.offset - prefixLength): manager.getHintText(it)]
+    }
+    assert actualParameterHints == expectedParameterHints
+  }
+
+  void 'test method call expressions'() {
+    prefix = '''\
+def foo = new Foo()
+foo.'''
+    testInlays 'simple(null)', [7: 'a']
+    testInlays 'defaultArgs(1, 2)', [12: 'a', 15: 'c']
+    testInlays 'defaultArgs(1, 2, 3)', [12: 'a', 15: 'b', 18: 'c']
+    testInlays 'mapArgs(foo: 1, null, bar: 2)', [16: 'a']
+    testInlays 'varArgs(1, 2, 3, 4)', [8: 'a', 11: '...b']
+    testInlays 'combo(1, foo: 10, 2, 3, 4, bar: 20, 5)', [6: 'a', 18: 'b', 21: '...c']
+  }
+
+  void 'test method call applications'() {
+    prefix = '''\
+def foo = new Foo()
+foo.'''
+    testInlays 'simple null', [7: 'a']
+    testInlays 'defaultArgs 1, 2', [12: 'a', 15: 'c']
+    testInlays 'defaultArgs 1, 2, 3', [12: 'a', 15: 'b', 18: 'c']
+    testInlays 'mapArgs foo: 1, null, bar: 2 ', [16: 'a']
+    testInlays 'varArgs 1, 2, 3, 4 ', [8: 'a', 11: '...b']
+    testInlays 'combo 1, foo: 10, 2, 3, 4, bar: 20, 5', [6: 'a', 18: 'b', 21: '...c']
+  }
+
+  void 'test index expression'() {
+    testInlays 'new Foo()[1]', [10: 'a']
+  }
+
+  void 'test new expression'() {
+    testInlays 'new Foo(1, 2, 4)', [8: 'a', 11: 'b', 14: 'c']
+  }
+
+  void 'test constructor invocation'() {
+    testInlays '''\
+class Bar {
+  Bar() {
+    this(1, 4, 5)
+  }
+
+  Bar(a, b, c) {}
+}
+''', [31: 'a', 34: 'b', 37: 'c']
+  }
+
+  void 'test enum constants'() {
+    testInlays '''\
+enum Baz {
+  ONE(1, 2),
+  TWO(4, 3)
+  Baz(a, b) {}
+}
+''', [17: 'a', 20: 'b', 30: 'a', 33: 'b']
+  }
+
+  void 'test no DGM inlays'() {
+    testInlays '[].each {}', [:]
+  }
+}