Implement Kotlin sub-classes tooltip to handle MPP correctly

Before this commit, Java tooltip was in use which could not
work correctly with expect / actual classes
So #KT-21011 Fixed
This commit is contained in:
Mikhail Glukhikh
2018-06-09 13:29:13 +03:00
parent 90b6f369e0
commit cbcd35e2f5
13 changed files with 114 additions and 6 deletions
+1
View File
@@ -477,6 +477,7 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
<codeInsight.template.postfixTemplateProvider language="kotlin" <codeInsight.template.postfixTemplateProvider language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/> implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/>
<codeInsight.linkHandler prefix="#kotlinClass/" handlerClass="org.jetbrains.kotlin.idea.highlighter.markers.KotlinClassTooltipLinkHandler"/>
<typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/> <typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/>
+2
View File
@@ -478,6 +478,8 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/> implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/>
<codeInsight.linkHandler prefix="#kotlinClass/" handlerClass="org.jetbrains.kotlin.idea.highlighter.markers.KotlinClassTooltipLinkHandler"/>
<typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/> <typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/>
<completion.contributor language="kotlin" <completion.contributor language="kotlin"
+2
View File
@@ -478,6 +478,8 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/> implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/>
<codeInsight.linkHandler prefix="#kotlinClass/" handlerClass="org.jetbrains.kotlin.idea.highlighter.markers.KotlinClassTooltipLinkHandler"/>
<typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/> <typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/>
<completion.contributor language="kotlin" <completion.contributor language="kotlin"
+2
View File
@@ -479,6 +479,8 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/> implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/>
<codeInsight.linkHandler prefix="#kotlinClass/" handlerClass="org.jetbrains.kotlin.idea.highlighter.markers.KotlinClassTooltipLinkHandler"/>
<typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/> <typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/>
<completion.contributor language="kotlin" <completion.contributor language="kotlin"
+2
View File
@@ -478,6 +478,8 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/> implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/>
<codeInsight.linkHandler prefix="#kotlinClass/" handlerClass="org.jetbrains.kotlin.idea.highlighter.markers.KotlinClassTooltipLinkHandler"/>
<typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/> <typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/>
<completion.contributor language="kotlin" <completion.contributor language="kotlin"
+2
View File
@@ -478,6 +478,8 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/> implementationClass="org.jetbrains.kotlin.idea.codeInsight.postfix.KtPostfixTemplateProvider"/>
<codeInsight.linkHandler prefix="#kotlinClass/" handlerClass="org.jetbrains.kotlin.idea.highlighter.markers.KotlinClassTooltipLinkHandler"/>
<typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/> <typeDeclarationProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinTypeDeclarationProvider"/>
<completion.contributor language="kotlin" <completion.contributor language="kotlin"
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.highlighter.markers
import com.intellij.codeInsight.highlighting.TooltipLinkHandler
import com.intellij.codeInsight.navigation.NavigationUtil
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.ModuleManager
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
class KotlinClassTooltipLinkHandler : TooltipLinkHandler() {
override fun handleLink(refSuffix: String, editor: Editor): Boolean {
val project = editor.project ?: return false
val javaPsiFacade = JavaPsiFacade.getInstance(project)
val qualifiedName = refSuffix.substringAfterLast(":")
val scope = if (qualifiedName.length == refSuffix.length) {
GlobalSearchScope.allScope(project)
} else {
val moduleName = refSuffix.substringBeforeLast(":")
val module = ModuleManager.getInstance(project).findModuleByName(moduleName)
module?.let { GlobalSearchScope.moduleScope(it) } ?: GlobalSearchScope.allScope(project)
}
// Non-JVM classes cannot be found with Java PSI Facade
val aClassElement =
KotlinFullClassNameIndex.getInstance().get(qualifiedName, project, scope).firstOrNull()
?: javaPsiFacade.findClass(qualifiedName, scope)
?: return false
NavigationUtil.activateFileWithPsiElement(aClassElement)
return true
}
}
@@ -144,7 +144,7 @@ interface TestableLineMarkerNavigator {
private val SUBCLASSED_CLASS = MarkerType( private val SUBCLASSED_CLASS = MarkerType(
"SUBCLASSED_CLASS", "SUBCLASSED_CLASS",
{ getPsiClass(it)?.let { MarkerType.getSubclassedClassTooltip(it) } }, { getPsiClass(it)?.let(::getSubclassedClassTooltip) },
object : LineMarkerNavigator() { object : LineMarkerNavigator() {
override fun browse(e: MouseEvent?, element: PsiElement?) { override fun browse(e: MouseEvent?, element: PsiElement?) {
getPsiClass(element)?.let { getPsiClass(element)?.let {
@@ -144,7 +144,7 @@ interface TestableLineMarkerNavigator {
private val SUBCLASSED_CLASS = MarkerType( private val SUBCLASSED_CLASS = MarkerType(
"SUBCLASSED_CLASS", "SUBCLASSED_CLASS",
{ getPsiClass(it)?.let { MarkerType.getSubclassedClassTooltip(it) } }, { getPsiClass(it)?.let(::getSubclassedClassTooltip) },
object : LineMarkerNavigator() { object : LineMarkerNavigator() {
override fun browse(e: MouseEvent?, element: PsiElement?) { override fun browse(e: MouseEvent?, element: PsiElement?) {
getPsiClass(element)?.let { MarkerType.navigateToSubclassedClass(e, it, DeclarationByModuleRenderer()) } getPsiClass(element)?.let { MarkerType.navigateToSubclassedClass(e, it, DeclarationByModuleRenderer()) }
@@ -21,23 +21,31 @@ import com.intellij.codeInsight.daemon.impl.GutterIconTooltipHelper
import com.intellij.codeInsight.navigation.ListBackgroundUpdaterTask import com.intellij.codeInsight.navigation.ListBackgroundUpdaterTask
import com.intellij.ide.util.MethodCellRenderer import com.intellij.ide.util.MethodCellRenderer
import com.intellij.ide.util.PsiElementListCellRenderer import com.intellij.ide.util.PsiElementListCellRenderer
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.keymap.KeymapUtil
import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.DumbService
import com.intellij.psi.PsiClass import com.intellij.psi.*
import com.intellij.psi.PsiElement import com.intellij.psi.presentation.java.ClassPresentationUtil
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifier
import com.intellij.psi.search.PsiElementProcessor import com.intellij.psi.search.PsiElementProcessor
import com.intellij.psi.search.PsiElementProcessorAdapter import com.intellij.psi.search.PsiElementProcessorAdapter
import com.intellij.psi.search.searches.ClassInheritorsSearch
import com.intellij.psi.search.searches.FunctionalExpressionSearch
import com.intellij.util.CommonProcessors import com.intellij.util.CommonProcessors
import gnu.trove.THashSet import gnu.trove.THashSet
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.elements.KtLightMethod import org.jetbrains.kotlin.asJava.elements.KtLightMethod
import org.jetbrains.kotlin.asJava.elements.isTraitFakeOverride import org.jetbrains.kotlin.asJava.elements.isTraitFakeOverride
import org.jetbrains.kotlin.idea.presentation.DeclarationByModuleRenderer
import org.jetbrains.kotlin.idea.search.declarationsSearch.forEachDeclaredMemberOverride import org.jetbrains.kotlin.idea.search.declarationsSearch.forEachDeclaredMemberOverride
import org.jetbrains.kotlin.idea.search.declarationsSearch.forEachOverridingMethod import org.jetbrains.kotlin.idea.search.declarationsSearch.forEachOverridingMethod
import org.jetbrains.kotlin.idea.search.declarationsSearch.toPossiblyFakeLightMethods import org.jetbrains.kotlin.idea.search.declarationsSearch.toPossiblyFakeLightMethods
import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.idea.util.isExpectDeclaration
import org.jetbrains.kotlin.idea.util.projectStructure.module
import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.util.* import java.util.*
import javax.swing.JComponent import javax.swing.JComponent
@@ -66,6 +74,45 @@ internal fun <T> getOverriddenDeclarations(mappingToJava: MutableMap<PsiElement,
return overridden return overridden
} }
// Module-specific version of MarkerType.getSubclassedClassTooltip
fun getSubclassedClassTooltip(klass: PsiClass): String? {
val processor = PsiElementProcessor.CollectElementsWithLimit(5, THashSet<PsiClass>())
ClassInheritorsSearch.search(klass).forEach(PsiElementProcessorAdapter(processor))
if (processor.isOverflow) {
return DaemonBundle.message(if (klass.isInterface) "interface.is.implemented.too.many" else "class.is.subclassed.too.many")
}
val subclasses = processor.toArray(PsiClass.EMPTY_ARRAY)
if (subclasses.isEmpty()) {
val functionalImplementations = PsiElementProcessor.CollectElementsWithLimit(2, THashSet<PsiFunctionalExpression>())
FunctionalExpressionSearch.search(klass).forEach(PsiElementProcessorAdapter(functionalImplementations))
return if (functionalImplementations.collection.isNotEmpty()) "Has functional implementations" else null
}
val start = DaemonBundle.message(if (klass.isInterface) "interface.is.implemented.by.header" else "class.is.subclassed.by.header")
val shortcuts = ActionManager.getInstance().getAction(IdeActions.ACTION_GOTO_IMPLEMENTATION).shortcutSet.shortcuts
val shortcut = shortcuts.firstOrNull()
var postfix = "<br><div style='margin-top: 5px'><font size='2'>Click"
if (shortcut != null) postfix += " or press " + KeymapUtil.getShortcutText(shortcut)
postfix += " to navigate</font></div>"
val renderer = DeclarationByModuleRenderer()
val comparator = renderer.comparator
return subclasses.toList().sortedWith(comparator).joinToString(
prefix = "<html><body>$start", postfix = "$postfix</body</html>", separator = "<br>"
) {
val moduleNameRequired = if (it is KtLightClass) {
val origin = it.kotlinOrigin
origin?.hasActualModifier() == true || origin?.isExpectDeclaration() == true
} else false
val moduleName = it.module?.name
val elementText = renderer.getElementText(it) + (moduleName?.takeIf { moduleNameRequired }?.let { " [$it]" } ?: "")
val refText = (moduleName?.let { "$it:" } ?: "") + ClassPresentationUtil.getNameForClass(it, /* qualified = */ true)
"&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#kotlinClass/$refText\">$elementText</a>"
}
}
fun getOverriddenMethodTooltip(method: PsiMethod): String? { fun getOverriddenMethodTooltip(method: PsiMethod): String? {
val processor = PsiElementProcessor.CollectElementsWithLimit<PsiMethod>(5) val processor = PsiElementProcessor.CollectElementsWithLimit<PsiMethod>(5)
method.forEachOverridingMethod(processor = PsiElementProcessorAdapter(processor)::process) method.forEachOverridingMethod(processor = PsiElementProcessorAdapter(processor)::process)
@@ -0,0 +1,6 @@
open class <lineMarker descr="<html><body>Is subclassed by<br>&nbsp;&nbsp;&nbsp;&nbsp;<a href="#kotlinClass/testModule_Common:ExpectedChild">ExpectedChild [testModule_Common]</a><br>&nbsp;&nbsp;&nbsp;&nbsp;<a href="#kotlinClass/testModule_JVM:ExpectedChild">ExpectedChild [testModule_JVM]</a><br>&nbsp;&nbsp;&nbsp;&nbsp;<a href="#kotlinClass/testModule_Common:SimpleChild">SimpleChild</a><br><div style='margin-top: 5px'><font size='2'>Click or press Ctrl+Alt+B to navigate</font></div></body</html>">SimpleParent</lineMarker>
expect class <lineMarker descr="Has actuals in JVM">ExpectedChild</lineMarker> : SimpleParent
class SimpleChild : SimpleParent()
@@ -0,0 +1,3 @@
// !CHECK_HIGHLIGHTING
actual class ExpectedChild : SimpleParent()
@@ -25,6 +25,11 @@ public class MultiModuleLineMarkerTestGenerated extends AbstractMultiModuleLineM
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
} }
@TestMetadata("actualDerived")
public void testActualDerived() throws Exception {
runTest("idea/testData/multiModuleLineMarker/actualDerived/");
}
@TestMetadata("actualEnumEntries") @TestMetadata("actualEnumEntries")
public void testActualEnumEntries() throws Exception { public void testActualEnumEntries() throws Exception {
runTest("idea/testData/multiModuleLineMarker/actualEnumEntries/"); runTest("idea/testData/multiModuleLineMarker/actualEnumEntries/");