KT-38027 Support Code Vision feature in Kotlin

This commit is contained in:
Andrei Klunnyi
2020-04-30 11:58:50 +02:00
parent 4c8f9e4e06
commit b08f501aac
30 changed files with 2531 additions and 3 deletions
@@ -40,6 +40,7 @@ import org.jetbrains.kotlin.idea.AbstractSmartSelectionTest
import org.jetbrains.kotlin.idea.actions.AbstractGotoTestOrCodeActionTest
import org.jetbrains.kotlin.idea.caches.resolve.*
import org.jetbrains.kotlin.idea.codeInsight.*
import org.jetbrains.kotlin.idea.codeInsight.codevision.AbstractKotlinCodeVisionProviderTest
import org.jetbrains.kotlin.idea.codeInsight.generate.AbstractCodeInsightActionTest
import org.jetbrains.kotlin.idea.codeInsight.generate.AbstractGenerateHashCodeAndEqualsActionTest
import org.jetbrains.kotlin.idea.codeInsight.generate.AbstractGenerateTestSupportMethodActionTest
@@ -867,6 +868,10 @@ fun main() {
model("codeInsight/postfix")
}
testClass<AbstractKotlinCodeVisionProviderTest> {
model("codeInsight/codeVision")
}
testClass<AbstractScriptConfigurationHighlightingTest> {
model("script/definition/highlighting", extension = null, recursive = false)
model("script/definition/complex", extension = null, recursive = false, testMethod = "doComplexTest")
File diff suppressed because it is too large Load Diff
@@ -2231,3 +2231,18 @@ title.import.layout=Import Layout
title.packages.to.use.import.with=Packages to Use Import with '*'
redundant.qualifier.unnecessary.non.direct.parent.class.qualifier=Unnecessary non-direct parent classes qualifiers
fix.add.exception.to.throws=Add ''{0}''
hints.title.codevision=Code Vision
hints.title.codevision.show.hints.for=Show hints for:
hints.title.codevision.usages=Usages
hints.title.codevision.inheritors=Implementations/Inheritors/Overrides
hints.codevision.usages.format={0, choice, 1#1 Usage|2#{0,number} Usages}
hints.codevision.usages.too_many.format={0,number}+ Usages
hints.codevision.implementations.format={0, choice, 1#1 Implementation|2#{0,number} Implementations}
hints.codevision.implementations.too_many.format={0,number}+ Implementations
hints.codevision.inheritors.format={0, choice, 1#1 Inheritor|2#{0,number} Inheritors}
hints.codevision.inheritors.to_many.format={0,number}+ Inheritors
hints.codevision.overrides.format={0, choice, 1#1 Override|2#{0,number} Overrides}
hints.codevision.overrides.to_many.format={0,number}+ Overrides
hints.codevision.settings=Settings...
@@ -3374,6 +3374,9 @@
<stripTrailingSpacesFilterFactory implementation="org.jetbrains.kotlin.idea.editor.KotlinStripTrailingSpacesFilterFactory"/>
<navbar implementation="org.jetbrains.kotlin.idea.navigationToolbar.KotlinNavBarModelExtension" order="first"/>
<statistics.counterUsagesCollector groupId="kotlin.code.vision" version="1"/>
<codeInsight.inlayProvider language="kotlin" implementationClass="org.jetbrains.kotlin.idea.codeInsight.codevision.KotlinCodeVisionProvider"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
@@ -0,0 +1,53 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("UnstableApiUsage")
package org.jetbrains.kotlin.idea.codeInsight.codevision
import com.intellij.codeInsight.hints.ChangeListener
import com.intellij.codeInsight.hints.ImmediateConfigurable
import com.intellij.codeInsight.hints.settings.InlayHintsConfigurable
import com.intellij.internal.statistic.eventLog.FeatureUsageData
import com.intellij.internal.statistic.service.fus.collectors.FUCounterUsageLogger
import com.intellij.openapi.project.Project
import com.intellij.ui.layout.panel
import org.jetbrains.kotlin.idea.KotlinBundle
import javax.swing.JPanel
// FIX ME WHEN BUNCH 192 REMOVED
typealias CodeVisionInlayHintsConfigurable = InlayHintsConfigurable
fun logUsageStatistics(project: Project?, groupId: String, eventId: String) =
FUCounterUsageLogger.getInstance().logEvent(project, groupId, eventId)
fun logUsageStatistics(project: Project?, groupId: String, eventId: String, data: FeatureUsageData) =
FUCounterUsageLogger.getInstance().logEvent(project, groupId, eventId, data)
fun createImmediateConfigurable(settings: KotlinCodeVisionProvider.KotlinCodeVisionSettings): ImmediateConfigurable {
return object : ImmediateConfigurable {
override fun createComponent(listener: ChangeListener): JPanel {
return panel {}
}
override val cases: List<ImmediateConfigurable.Case>
get() = listOf(
ImmediateConfigurable.Case(
KotlinBundle.message("hints.title.codevision.usages"),
"usages",
settings::showUsages
),
ImmediateConfigurable.Case(
KotlinBundle.message("hints.title.codevision.inheritors"),
"inheritors",
settings::showInheritors
)
)
override val mainCheckboxText: String
get() = KotlinBundle.message("hints.title.codevision.show.hints.for")
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.codeInsight.codevision
import com.intellij.codeInsight.hints.ChangeListener
import com.intellij.codeInsight.hints.ImmediateConfigurable
import com.intellij.codeInsight.hints.config.InlayHintsConfigurable
import com.intellij.internal.statistic.eventLog.FeatureUsageData
import com.intellij.internal.statistic.service.fus.collectors.FUCounterUsageLogger
import com.intellij.openapi.project.Project
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.layout.panel
import org.jetbrains.kotlin.idea.KotlinBundle
import javax.swing.JPanel
typealias CodeVisionInlayHintsConfigurable = InlayHintsConfigurable
fun logUsageStatistics(project: Project?, groupId: String, eventId: String) {
project?.let { FUCounterUsageLogger.getInstance().logEvent(project, groupId, eventId) }
}
fun logUsageStatistics(project: Project?, groupId: String, eventId: String, data: FeatureUsageData) {
project?.let { FUCounterUsageLogger.getInstance().logEvent(project, groupId, eventId, data) }
}
fun createImmediateConfigurable(settings: KotlinCodeVisionProvider.KotlinCodeVisionSettings): ImmediateConfigurable {
return object : ImmediateConfigurable {
val usagesText = KotlinBundle.message("hints.title.codevision.usages")
val inheritorsText = KotlinBundle.message("hints.title.codevision.inheritors")
private val usagesField = JBCheckBox(usagesText, settings.showUsages)
private val inheritorsField = JBCheckBox(inheritorsText, settings.showInheritors)
override fun createComponent(listener: ChangeListener): JPanel {
usagesField.isSelected = settings.showUsages
usagesField.addActionListener { settings.showUsages = usagesField.isSelected }
inheritorsField.isSelected = settings.showInheritors
inheritorsField.addActionListener { settings.showInheritors = inheritorsField.isSelected }
return panel {
row { usagesField(pushX) }
row { inheritorsField(pushX) }
}
}
}
}
@@ -0,0 +1,262 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.codeInsight.codevision
import com.intellij.codeInsight.hints.FactoryInlayHintsCollector
import com.intellij.codeInsight.hints.InlayHintsSink
import com.intellij.codeInsight.hints.presentation.AttributesTransformerPresentation
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.MouseButton
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootModificationTracker
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.DirectClassInheritorsSearch
import com.intellij.psi.search.searches.MethodReferencesSearch
import com.intellij.psi.search.searches.OverridingMethodsSearch
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.util.ArrayUtil
import com.intellij.util.Processor
import org.jetbrains.kotlin.asJava.LightClassUtil
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.idea.refactoring.isAbstract
import org.jetbrains.kotlin.idea.search.declarationsSearch.toPossiblyFakeLightMethods
import org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinDefinitionsSearcher
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtFunctionLiteral
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.utils.SmartList
@Suppress("UnstableApiUsage")
class KotlinCodeVisionHintsCollector(
editor: Editor, private val showUsages: Boolean, private val showInheritors: Boolean,
private val usagesLimit: Int, private val inheritorsLimit: Int
) : FactoryInlayHintsCollector(editor) {
companion object {
private val hintsKey = Key.create<CachedValue<CachedHints>>("CodeVisionHints")
fun searchUsages(element: PsiElement, usagesLimit: Int): Usages? {
val countingProcessor = CountingUpToLimitProcessor<Any>(usagesLimit)
if (element is KtClass) {
ReferencesSearch.search(element).forEach(countingProcessor)
} else {
element.toPossiblyFakeLightMethods().firstOrNull() // inline methods don't have light analogues (fake needed)
?.let { MethodReferencesSearch.search(it).forEach(countingProcessor) }
}
val (usagesNum, limitReached) = countingProcessor
return if (usagesNum > 0) Usages(usagesNum, limitReached) else null
}
private fun searchFunctionOverrides(function: KtFunction, inheritorsLimit: Int): KotlinCodeVisionLimitedHint? {
return LightClassUtil.getLightClassMethod(function)?.let { it ->
val countingProcessor = CountingUpToLimitProcessor<Any>(inheritorsLimit)
OverridingMethodsSearch.search(it, true).forEach(countingProcessor)
val (overridingNum, limitReached) = countingProcessor
if (overridingNum > 0) {
if (function.isAbstract()) FunctionImplementations(overridingNum, limitReached)
else FunctionOverrides(overridingNum, limitReached)
} else null
}
}
private fun searchClassInheritors(clazz: KtClass, inheritorsLimit: Int): KotlinCodeVisionLimitedHint? {
return clazz.toLightClass()?.let {
val countingProcessor = CountingUpToLimitProcessor<Any>(inheritorsLimit)
DirectClassInheritorsSearch.search(it, clazz.useScope, true).forEach(countingProcessor)
val (inheritorsNum, limitReached) = countingProcessor
if (inheritorsNum > 0) {
if (clazz.isInterface()) InterfaceImplementations(inheritorsNum, limitReached)
else ClassInheritors(inheritorsNum, limitReached)
} else null
}
}
private fun searchPropertyOverriding(property: KtProperty, inheritorsLimit: Int): KotlinCodeVisionLimitedHint? {
val countingProcessor = CountingUpToLimitProcessor<PsiElement>(inheritorsLimit)
KotlinDefinitionsSearcher.processPropertyImplementationsMethods(
property.toPossiblyFakeLightMethods(),
GlobalSearchScope.allScope(property.project),
countingProcessor
)
val (overridesNum, limitReached) = countingProcessor
return if (overridesNum > 0) PropertyOverrides(overridesNum, limitReached) else null
}
}
override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
if (!showUsages && !showInheritors) return false
if (!isElementOfInterest(element)) return true
if (editor.project == null) return false
val hints = retrieveHints(editor.project!!, element)
val inlayPresentations =
hints.takeIf { it.isNotEmpty() }?.let { listOf(prepareBlockElements(element, editor, hints)) } ?: emptyList()
inlayPresentations.forEach { hintPair ->
sink.addBlockElement(
hintPair.first,
relatesToPrecedingText = true,
showAbove = true,
priority = 0,
presentation = hintPair.second
)
}
return true
}
private fun retrieveHints(project: Project, element: PsiElement): List<KotlinCodeVisionLimitedHint> {
val cachedValueProvider = CachedHintsProvider(
element, showUsages, showInheritors, usagesLimit, inheritorsLimit
)
return CachedValuesManager.getManager(project).run {
this.getCachedValue(element, hintsKey, cachedValueProvider, false).takeUnless { isStale(it) } ?: run {
this.createCachedValue(cachedValueProvider).also { element.putUserData(hintsKey, it) }.value
}
}.hints
}
private fun isStale(cachedValue: CachedHints): Boolean {
return cachedValue.showUsages != showUsages || cachedValue.showInheritors != showInheritors
}
@Suppress("GrazieInspection")
private fun prepareBlockElements(
element: PsiElement,
editor: Editor,
hints: List<KotlinCodeVisionHint>
): Pair<Int, InlayPresentation> {
assert(hints.isNotEmpty()) { "Attempt to build block elements whereas hints don't exist" }
assert(hints.size <= 2) { "Hints other than usages-implementations are not expected" }
val offset = element.textRange.startOffset
val line = editor.document.getLineNumber(offset)
val lineStart = editor.document.getLineStartOffset(line)
val indent = offset - lineStart
/*
* presentations: <indent>[<Usages>][<space><Inheritors>]
* hints: hint[0] hint[1]
*/
val presentations = arrayOfNulls<InlayPresentation>(hints.size * 2) // 2 or 4
presentations[0] = factory.text(StringUtil.repeat(" ", indent))
var pInd = 1
for (hInd in hints.indices) { // handling usages & inheritors
val hint: KotlinCodeVisionHint = hints[hInd]
if (hInd != 0)
presentations[pInd++] = factory.text(" ")
presentations[pInd++] = createPresentation(factory, element, editor, hint) // either Usages or Inheritors
}
val filledPresentations = presentations.requireNoNulls()
val seq = factory.seq(*filledPresentations)
val withAppearingSettings = factory.changeOnHover(seq, {
val spaceAndSettings = arrayOf(factory.text(" "), createSettings(factory, element, editor))
val withSettings = ArrayUtil.mergeArrays(filledPresentations, spaceAndSettings)
factory.seq(*withSettings)
}) { true }
return Pair(lineStart, withAppearingSettings)
}
private fun isElementOfInterest(element: PsiElement): Boolean =
element is KtClass
|| (element is KtFunction && element !is KtFunctionLiteral)
|| (element is KtProperty && !element.isLocal)
private fun createPresentation(
factory: PresentationFactory, element: PsiElement, editor: Editor, result: KotlinCodeVisionHint
): InlayPresentation {
val text = factory.smallText(result.regularText)
return factory.changeOnHover(text, {
val onClick = factory.onClick(text, MouseButton.Left)
{ event, _ -> result.onClick(editor, element, event) }
applyReferenceColor(onClick)
}) { true }
}
private fun applyReferenceColor(presentation: InlayPresentation): InlayPresentation {
return AttributesTransformerPresentation(presentation) {
val attributes = EditorColorsManager.getInstance()
.globalScheme.getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR).clone()
attributes.apply { effectType = null }
}
}
private fun createSettings(factory: PresentationFactory, element: PsiElement, editor: Editor): InlayPresentation {
return createPresentation(factory, element, editor, SettingsHint())
}
class CachedHintsProvider(
private val element: PsiElement,
private val showUsages: Boolean,
private val showInheritors: Boolean,
private val usagesLimit: Int,
private val inheritorsLimit: Int
) : CachedValueProvider<CachedHints> {
override fun compute(): CachedValueProvider.Result<CachedHints>? {
val hints: MutableList<KotlinCodeVisionLimitedHint> = SmartList()
if (showUsages)
searchUsages(element, usagesLimit)?.let { hints += it }
if (showInheritors) {
when (element) {
is KtFunction -> searchFunctionOverrides(element, inheritorsLimit)?.let { hints += it }
is KtClass -> searchClassInheritors(element, inheritorsLimit)?.let { hints += it }
is KtProperty -> searchPropertyOverriding(element, inheritorsLimit)?.let { hints += it }
}
}
return CachedValueProvider.Result(
CachedHints(hints, this.showUsages, this.showInheritors),
// kotlinOutOfCodeBlockTracker doesn't work for usages: new reference inside the block is not detected
// MODIFICATION_COUNT {+: file reopening -> cache hit, -: any code modification resets all caches (records are in PSI)}
PsiModificationTracker.MODIFICATION_COUNT,
ProjectRootModificationTracker.getInstance(element.project) // tracking project structure changes
)
}
}
class CountingUpToLimitProcessor<T>(private val limit: Int) : Processor<T> {
private val findings = mutableSetOf<T>() // for properties it's crucial not to calculate setters and getters together
override fun process(t: T): Boolean {
findings.add(t)
return findings.size < limit
}
operator fun component1(): Int = findings.size
operator fun component2(): Boolean = findings.size >= limit
}
data class CachedHints(
val hints: List<KotlinCodeVisionLimitedHint>,
val showUsages: Boolean,
val showInheritors: Boolean
)
}
@@ -0,0 +1,131 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.codeInsight.codevision
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.internal.statistic.eventLog.FeatureUsageData
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.highlighter.markers.OVERRIDDEN_FUNCTION
import org.jetbrains.kotlin.idea.highlighter.markers.OVERRIDDEN_PROPERTY
import org.jetbrains.kotlin.idea.highlighter.markers.SUBCLASSED_CLASS
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtProperty
import java.awt.event.MouseEvent
abstract class KotlinCodeVisionHint(hintKey: String) {
open val regularText: String = KotlinBundle.message(hintKey)
abstract fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?)
}
abstract class KotlinCodeVisionLimitedHint(num: Int, limitReached: Boolean, regularHintKey: String, tooManyHintKey: String) :
KotlinCodeVisionHint(regularHintKey) {
override val regularText: String =
if (limitReached) KotlinBundle.message(tooManyHintKey, num) else KotlinBundle.message(regularHintKey, num)
}
private const val IMPLEMENTATIONS_KEY = "hints.codevision.implementations.format"
private const val IMPLEMENTATIONS_TO_MANY_KEY = "hints.codevision.implementations.too_many.format"
private const val INHERITORS_KEY = "hints.codevision.inheritors.format"
private const val INHERITORS_TO_MANY_KEY = "hints.codevision.inheritors.to_many.format"
private const val OVERRIDES_KEY = "hints.codevision.overrides.format"
private const val OVERRIDES_TOO_MANY_KEY = "hints.codevision.overrides.to_many.format"
private const val USAGES_KEY = "hints.codevision.usages.format"
private const val USAGES_TOO_MANY_KEY = "hints.codevision.usages.too_many.format"
private const val SETTINGS_FORMAT = "hints.codevision.settings"
// FUS = Feature Usage Statistics
const val FUS_GROUP_ID = "kotlin.code.vision"
const val USAGES_CLICKED_EVENT_ID = "usages.clicked"
const val INHERITORS_CLICKED_EVENT_ID = "inheritors.clicked"
const val SETTING_CLICKED_EVENT_ID = "setting.clicked"
// FUD = Feature Usage Data
const val FUD_KEY = "location"
const val FUD_FUNCTION = "function"
const val FUD_PROPERTY = "property"
const val FUD_CLASS = "class"
const val FUD_INTERFACE = "interface"
class Usages(usagesNum: Int, limitReached: Boolean) :
KotlinCodeVisionLimitedHint(usagesNum, limitReached, USAGES_KEY, USAGES_TOO_MANY_KEY) {
override fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
logUsageStatistics(editor.project, FUS_GROUP_ID, USAGES_CLICKED_EVENT_ID)
GotoDeclarationAction.startFindUsages(editor, editor.project!!, element)
}
}
class FunctionOverrides(overridesNum: Int, limitReached: Boolean) :
KotlinCodeVisionLimitedHint(overridesNum, limitReached, OVERRIDES_KEY, OVERRIDES_TOO_MANY_KEY) {
override fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
val data = FeatureUsageData().addData(FUD_KEY, FUD_FUNCTION)
logUsageStatistics(editor.project, FUS_GROUP_ID, INHERITORS_CLICKED_EVENT_ID, data)
val navigationHandler = OVERRIDDEN_FUNCTION.navigationHandler
navigationHandler.navigate(event, (element as KtFunction).nameIdentifier)
}
}
class FunctionImplementations(implNum: Int, limitReached: Boolean) :
KotlinCodeVisionLimitedHint(implNum, limitReached, IMPLEMENTATIONS_KEY, IMPLEMENTATIONS_TO_MANY_KEY) {
override fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
val data = FeatureUsageData().addData(FUD_KEY, FUD_FUNCTION)
logUsageStatistics(editor.project, FUS_GROUP_ID, INHERITORS_CLICKED_EVENT_ID, data)
val navigationHandler = OVERRIDDEN_FUNCTION.navigationHandler
navigationHandler.navigate(event, (element as KtFunction).nameIdentifier)
}
}
class PropertyOverrides(overridesNum: Int, limitReached: Boolean) :
KotlinCodeVisionLimitedHint(overridesNum, limitReached, OVERRIDES_KEY, OVERRIDES_TOO_MANY_KEY) {
override fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
val data = FeatureUsageData().addData(FUD_KEY, FUD_PROPERTY)
logUsageStatistics(editor.project, FUS_GROUP_ID, INHERITORS_CLICKED_EVENT_ID, data)
val navigationHandler = OVERRIDDEN_PROPERTY.navigationHandler
navigationHandler.navigate(event, (element as KtProperty).nameIdentifier)
}
}
class ClassInheritors(inheritorsNum: Int, limitReached: Boolean) :
KotlinCodeVisionLimitedHint(inheritorsNum, limitReached, INHERITORS_KEY, INHERITORS_TO_MANY_KEY) {
override fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
val data = FeatureUsageData().addData(FUD_KEY, FUD_CLASS)
logUsageStatistics(editor.project, FUS_GROUP_ID, INHERITORS_CLICKED_EVENT_ID, data)
val navigationHandler = SUBCLASSED_CLASS.navigationHandler
navigationHandler.navigate(event, (element as KtClass).nameIdentifier)
}
}
class InterfaceImplementations(implNum: Int, limitReached: Boolean) :
KotlinCodeVisionLimitedHint(implNum, limitReached, IMPLEMENTATIONS_KEY, IMPLEMENTATIONS_TO_MANY_KEY) {
override fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
val data = FeatureUsageData().addData(FUD_KEY, FUD_INTERFACE)
logUsageStatistics(editor.project, FUS_GROUP_ID, INHERITORS_CLICKED_EVENT_ID, data)
val navigationHandler = SUBCLASSED_CLASS.navigationHandler
navigationHandler.navigate(event, (element as KtClass).nameIdentifier)
}
}
class SettingsHint : KotlinCodeVisionHint(SETTINGS_FORMAT) {
override fun onClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
val project = element.project
logUsageStatistics(project, FUS_GROUP_ID, SETTING_CLICKED_EVENT_ID)
CodeVisionInlayHintsConfigurable.showSettingsDialogForLanguage(project, element.language)
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.codeInsight.codevision
import com.intellij.codeInsight.hints.*
import com.intellij.lang.Language
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.KotlinLanguage
@Suppress("UnstableApiUsage")
class KotlinCodeVisionProvider : InlayHintsProvider<KotlinCodeVisionProvider.KotlinCodeVisionSettings> {
override val key: SettingsKey<KotlinCodeVisionSettings> = SettingsKey("CodeVision")
override val name: String = KotlinBundle.message("hints.title.codevision")
override val previewText: String? = null
var usagesLimit: Int = 100
var inheritorsLimit: Int = 100
override fun isLanguageSupported(language: Language): Boolean = language is KotlinLanguage
override fun createConfigurable(settings: KotlinCodeVisionSettings): ImmediateConfigurable = createImmediateConfigurable(settings)
override fun createSettings(): KotlinCodeVisionSettings = KotlinCodeVisionSettings()
override fun getCollectorFor(
file: PsiFile, editor: Editor, settings: KotlinCodeVisionSettings, sink: InlayHintsSink
): InlayHintsCollector? {
if (!settings.showUsages && !settings.showInheritors) return null
return KotlinCodeVisionHintsCollector(editor, settings.showUsages, settings.showInheritors, usagesLimit, inheritorsLimit)
}
data class KotlinCodeVisionSettings(var showUsages: Boolean = false, var showInheritors: Boolean = false)
}
@@ -144,7 +144,7 @@ interface TestableLineMarkerNavigator {
fun getTargetsPopupDescriptor(element: PsiElement?): NavigationPopupDescriptor?
}
private val SUBCLASSED_CLASS = MarkerType(
val SUBCLASSED_CLASS = MarkerType(
"SUBCLASSED_CLASS",
{ getPsiClass(it)?.let(::getSubclassedClassTooltip) },
object : LineMarkerNavigator() {
@@ -155,7 +155,7 @@ private val SUBCLASSED_CLASS = MarkerType(
}
})
private val OVERRIDDEN_FUNCTION = object : MarkerType(
val OVERRIDDEN_FUNCTION = object : MarkerType(
"OVERRIDDEN_FUNCTION",
{ getPsiMethod(it)?.let(::getOverriddenMethodTooltip) },
object : LineMarkerNavigator() {
@@ -176,7 +176,7 @@ private val OVERRIDDEN_FUNCTION = object : MarkerType(
}
}
private val OVERRIDDEN_PROPERTY = object : MarkerType(
val OVERRIDDEN_PROPERTY = object : MarkerType(
"OVERRIDDEN_PROPERTY",
{ it?.let { getOverriddenPropertyTooltip(it.parent as KtNamedDeclaration) } },
object : LineMarkerNavigator() {
@@ -0,0 +1,17 @@
// MODE: inheritors
<# block [ 2 Inheritors] #>
abstract class SomeClass {
<# block [ 1 Override] #>
open fun someFun() = ""
<# block [ 2 Implementations] #>
abstract fun someAbstractFun()
}
class DerivedClassA : SomeClass {
override fun someFun() = "overridden"
override fun someAbstractFun() = "overridden"
}
class DerivedClassB : SomeClass {
override fun someAbstractFun() = "overridden"
}
+16
View File
@@ -0,0 +1,16 @@
// MODE: inheritors
<# block [ 4 Inheritors] #>
open class SomeClass {
class NestedDerivedClass: SomeClass() {} // <== (1): nested class
}
<# block [ 1 Inheritor] #>
open class DerivedClass : SomeClass {} // <== (2): direct derived one
class AnotherDerivedClass : SomeClass {} // <== (3): yet another derived one
class DerivedDerivedClass : DerivedClass { // <== (): indirect inheritor of SomeClass
fun main() {
val someClassInstance = object : SomeClass() { // <== (4): anonymous derived one
val somethingHere = ""
}
}
}
@@ -0,0 +1,17 @@
// MODE: usages
<# block [ 1 Usage] #>
abstract class SomeClass {
<# block [ 3 Usages] #>
abstract fun someFun(): String
fun someOtherFun() = someFun() // <== (1): delegation from another method
val someProperty = someFun() // <== (2): property initializer
}
fun main() {
val instance = object: SomeClass {
<# block [ 1 Usage] #>
override fun someFun(): String {} // <== (): used below
}
instance.someFun() <== (3): call on an instance
}
@@ -0,0 +1,23 @@
// MODE: inheritors
<# block [ 1 Inheritor] #>
abstract class SomeClass {
<# block [ 1 Override] #>
abstract val someAbstractProperty: Int
<# block [ 2 Overrides] #>
open val nonAbstractProperty: Int = 10
open val notToBeOverriddenProperty: Int = 10
}
<# block [ 1 Inheritor] #>
open class DerivedClassA : SomeClass() {
override val someAbstractProperty: Int = 5
<# block [ 1 Override] #>
override val nonAbstractProperty: Int = 15 // NOTE that DerivedClassB overrides both getter and setter but counted once
}
class DerivedClassB : DerivedClassA() {
override var nonAbstractProperty: Int = 15
get() = 20
set(value) {field = value / 2}
}
@@ -0,0 +1,13 @@
// MODE: usages
<# block [ 1 Usage] #>
interface SomeClass {
<# block [ 3 Usages] #>
var someProperty = "initialized"
fun someFun() = "it's " + someProperty // <== (1): reference from expression
}
fun main() {
val instance = object: SomeClass {}
val someString = instance.someProperty // <== (2): getter call
instance.someProperty = "anotherValue" // <== (3): setter call
+11
View File
@@ -0,0 +1,11 @@
// MODE: usages
<# block [ 5 Usages] #>
open class SomeClass {}
class SomeOtherClass : SomeClass {} // <== (1): class extension
class SomeYetOtherClass : SomeClass { // <== (2): class extension
<# block [ 1 Usage] #>
fun acceptsClass(param: SomeClass) {} // <== (3): parameter type
fun returnsInterface(): SomeClass {} // <== (4): return type
fun main() = acceptsClass(object : SomeClass {}) // <== (5): anonymous class instance
}
@@ -0,0 +1,12 @@
// MODE: usages
<# block [ 3 Usages] #>
fun function(param: String): Int = 1
<# block [ 1 Usage] #>
fun higherOrderFun(s: String, param: (String) -> Int) = param(s)
fun main() {
function("someString")
val functionRef = ::function
higherOrderFun("someString", ::function)
}
@@ -0,0 +1,16 @@
// MODE: inheritors
<# block [ 1 Implementation] #>
interface SomeInterface {
<# block [ 2 Implementations] #>
fun interfaceMethodA()
}
<# block [ 1 Inheritor] #>
open class SomeClass : SomeInterface {
<# block [ 1 Override] #>
override fun interfaceMethodA() {} // <== (1)
}
class SomeDerivedClass : SomeClass() {
override fun interfaceMethodA() {} // <== (2)
}
@@ -0,0 +1,9 @@
// MODE: inheritors
<# block [ 3 Implementations] #>
interface SomeInterface {}
interface SomeOtherInterface : SomeInterface {} // <== (1): interface extension
class SomeClass : SomeInterface { // <== (2): interface implementation
fun acceptsInterface(param: SomeInterface) {}
fun main() = acceptsInterface(object : SomeInterface {}) // <== (3): anonymous class instance
}
@@ -0,0 +1,17 @@
// MODE: usages
<# block [ 1 Usage] #>
interface SomeInterface {
<# block [ 3 Usages] #>
fun someFun(): String
fun someOtherFun() = someFun() // <== (1): delegation from another interface method
val someProperty = someFun() // <== (2): property initializer
}
fun main() {
val instance = object: SomeInterface {
<# block [ 1 Usage] #>
override fun someFun(): String {} // <== (): used below
}
instance.someFun() <== (3): call on an instance
}
@@ -0,0 +1,11 @@
// MODE: inheritors
<# block [ 1 Implementation] #>
interface SomeInterface {
<# block [ 1 Override] #>
fun interfaceMethodA() = 10
}
class SomeClass : SomeInterface {
override fun interfaceMethodA() = 20 // <== (1)
}
@@ -0,0 +1,11 @@
// MODE: inheritors
<# block [ 1 Implementation] #>
interface SomeInterface {
<# block [ 1 Override] #>
open val interfaceProperty: String
}
class SomeClass : SomeInterface {
override val interfaceProperty: String = "overridden" // <== (1)
}
@@ -0,0 +1,12 @@
// MODE: usages
<# block [ 1 Usage] #>
interface SomeInterface {
<# block [ 2 Usages] #>
val someProperty = "initialized"
fun someFun() = "it's " + someProperty // <== (1):
}
fun main() {
val instance = object: SomeInterface {}
val someString = instance.someProperty // <== (2): call on an instance
+11
View File
@@ -0,0 +1,11 @@
// MODE: usages
<# block [ 5 Usages] #>
interface SomeInterface {}
interface SomeOtherInterface : SomeInterface {} // <== (1): interface extension
class SomeClass : SomeInterface { // <== (2): interface implementation
<# block [ 1 Usage] #>
fun acceptsInterface(param: SomeInterface) {} // <== (3): parameter type
fun returnsInterface(): SomeInterface {} // <== (4): return type
fun main() = acceptsInterface(object : SomeInterface {}) // <== (5): anonymous class instance
}
@@ -0,0 +1,18 @@
// MODE: usages-&-inheritors
// USAGES-LIMIT: 3
// INHERITORS-LIMIT: 2
<# block [ 3+ Usages 2+ Inheritors] #>
open class SomeClass {
class NestedDerivedClass: SomeClass() {} // <== (1): nested class
}
<# block [ 1 Usage 1 Inheritor] #>
open class DerivedClass : SomeClass {} // <== (2): direct derived one
class AnotherDerivedClass : SomeClass {} // <== (3): yet another derived one
class DerivedDerivedClass : DerivedClass { // <== (): indirect inheritor of SomeClass
fun main() {
val someClassInstance = object : SomeClass() // { <== (4): anonymous derived one
val somethingHere = ""
}
}
}
@@ -0,0 +1,16 @@
// MODE: usages-&-inheritors
<# block [ 4 Usages 4 Inheritors] #>
open class SomeClass {
class NestedDerivedClass: SomeClass() {} // <== (1): nested class
}
<# block [ 1 Usage 1 Inheritor] #>
open class DerivedClass : SomeClass {} // <== (2): direct derived one
class AnotherDerivedClass : SomeClass {} // <== (3): yet another derived one
class DerivedDerivedClass : DerivedClass { // <== (): indirect inheritor of SomeClass
fun main() {
val someClassInstance = object : SomeClass() { // <== (4): anonymous derived one
val somethingHere = ""
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.codeInsight.codevision
import com.intellij.openapi.util.io.FileUtil
import com.intellij.testFramework.utils.inlays.InlayHintsProviderTestCase
import org.jetbrains.kotlin.idea.codeInsight.codevision.KotlinCodeVisionProvider.KotlinCodeVisionSettings
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import java.io.File
open class AbstractKotlinCodeVisionProviderTest : InlayHintsProviderTestCase() { // Abstract- prefix is just a convention for GenerateTests
fun doTest(testPath: String) { // named according to the convention imposed by GenerateTests
assertThatActualHintsMatch(testPath)
}
private fun assertThatActualHintsMatch(fileName: String) {
val fileContents = FileUtil.loadFile(File(fileName), true)
val usagesLimit = InTextDirectivesUtils.findStringWithPrefixes(fileContents, "// USAGES-LIMIT: ")?.toInt() ?: 100
val inheritorsLimit = InTextDirectivesUtils.findStringWithPrefixes(fileContents, "// INHERITORS-LIMIT: ")?.toInt() ?: 100
val codeVisionProvider = KotlinCodeVisionProvider()
codeVisionProvider.usagesLimit = usagesLimit
codeVisionProvider.inheritorsLimit = inheritorsLimit
val mode: KotlinCodeVisionSettings = when (InTextDirectivesUtils.findStringWithPrefixes(fileContents, "// MODE: ")) {
"inheritors" -> inheritorsEnabled()
"usages" -> usagesEnabled()
"usages-&-inheritors" -> usagesAndInheritorsEnabled()
else -> codeVisionDisabled()
}
testProvider("kotlinCodeVision.kt", fileContents, codeVisionProvider, mode)
}
private fun usagesAndInheritorsEnabled(): KotlinCodeVisionSettings = KotlinCodeVisionSettings(showUsages = true, showInheritors = true)
private fun inheritorsEnabled(): KotlinCodeVisionSettings = KotlinCodeVisionSettings(showUsages = false, showInheritors = true)
private fun usagesEnabled(): KotlinCodeVisionSettings = KotlinCodeVisionSettings(showUsages = true, showInheritors = false)
private fun codeVisionDisabled(): KotlinCodeVisionSettings = KotlinCodeVisionSettings(showUsages = false, showInheritors = false)
}
@@ -0,0 +1 @@
package org.jetbrains.kotlin.idea.codeInsight.codevision
@@ -0,0 +1,110 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.codeInsight.codevision;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("idea/testData/codeInsight/codeVision")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class KotlinCodeVisionProviderTestGenerated extends AbstractKotlinCodeVisionProviderTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInCodeVision() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/codeInsight/codeVision"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@TestMetadata("ClassFunctionOverrides.kt")
public void testClassFunctionOverrides() throws Exception {
runTest("idea/testData/codeInsight/codeVision/ClassFunctionOverrides.kt");
}
@TestMetadata("ClassInheritors.kt")
public void testClassInheritors() throws Exception {
runTest("idea/testData/codeInsight/codeVision/ClassInheritors.kt");
}
@TestMetadata("ClassMethodUsages.kt")
public void testClassMethodUsages() throws Exception {
runTest("idea/testData/codeInsight/codeVision/ClassMethodUsages.kt");
}
@TestMetadata("ClassPropertiesOverrides.kt")
public void testClassPropertiesOverrides() throws Exception {
runTest("idea/testData/codeInsight/codeVision/ClassPropertiesOverrides.kt");
}
@TestMetadata("ClassPropertyUsages.kt")
public void testClassPropertyUsages() throws Exception {
runTest("idea/testData/codeInsight/codeVision/ClassPropertyUsages.kt");
}
@TestMetadata("ClassUsages.kt")
public void testClassUsages() throws Exception {
runTest("idea/testData/codeInsight/codeVision/ClassUsages.kt");
}
@TestMetadata("GlobalFunctionUsages.kt")
public void testGlobalFunctionUsages() throws Exception {
runTest("idea/testData/codeInsight/codeVision/GlobalFunctionUsages.kt");
}
@TestMetadata("InterfaceAbstractMethodImplementation.kt")
public void testInterfaceAbstractMethodImplementation() throws Exception {
runTest("idea/testData/codeInsight/codeVision/InterfaceAbstractMethodImplementation.kt");
}
@TestMetadata("InterfaceImplementations.kt")
public void testInterfaceImplementations() throws Exception {
runTest("idea/testData/codeInsight/codeVision/InterfaceImplementations.kt");
}
@TestMetadata("InterfaceMethodUsages.kt")
public void testInterfaceMethodUsages() throws Exception {
runTest("idea/testData/codeInsight/codeVision/InterfaceMethodUsages.kt");
}
@TestMetadata("InterfaceMethodsOverrides.kt")
public void testInterfaceMethodsOverrides() throws Exception {
runTest("idea/testData/codeInsight/codeVision/InterfaceMethodsOverrides.kt");
}
@TestMetadata("InterfacePropertiesOverrides.kt")
public void testInterfacePropertiesOverrides() throws Exception {
runTest("idea/testData/codeInsight/codeVision/InterfacePropertiesOverrides.kt");
}
@TestMetadata("InterfacePropertyUsages.kt")
public void testInterfacePropertyUsages() throws Exception {
runTest("idea/testData/codeInsight/codeVision/InterfacePropertyUsages.kt");
}
@TestMetadata("InterfaceUsages.kt")
public void testInterfaceUsages() throws Exception {
runTest("idea/testData/codeInsight/codeVision/InterfaceUsages.kt");
}
@TestMetadata("TooManyUsagesAndInheritors.kt")
public void testTooManyUsagesAndInheritors() throws Exception {
runTest("idea/testData/codeInsight/codeVision/TooManyUsagesAndInheritors.kt");
}
@TestMetadata("UsagesAndInheritanceTogether.kt")
public void testUsagesAndInheritanceTogether() throws Exception {
runTest("idea/testData/codeInsight/codeVision/UsagesAndInheritanceTogether.kt");
}
}
@@ -0,0 +1 @@
package org.jetbrains.kotlin.idea.codeInsight.codevision;