Initial implementation of rainbow highlighting for Kotlin

#KT-12629 Fixed
This commit is contained in:
Dmitry Jemerov
2017-04-21 18:34:19 +02:00
parent c9769ab454
commit 5315f4d9da
5 changed files with 203 additions and 2 deletions
@@ -16,11 +16,16 @@
package org.jetbrains.kotlin.idea.highlighter package org.jetbrains.kotlin.idea.highlighter
import com.intellij.codeHighlighting.RainbowHighlighter
import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag
import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink
import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtExpressionWithLabel import org.jetbrains.kotlin.psi.KtExpressionWithLabel
import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtLambdaExpression
@@ -34,7 +39,7 @@ internal class BeforeResolveHighlightingVisitor(private val holder: AnnotationHo
override fun visitElement(element: PsiElement) { override fun visitElement(element: PsiElement) {
val elementType = element.node.elementType val elementType = element.node.elementType
val attributes = when { val attributes = when {
element is KDocLink -> KotlinHighlightingColors.KDOC_LINK element is KDocLink && !willApplyRainbowHighlight(element) -> KotlinHighlightingColors.KDOC_LINK
elementType in KtTokens.SOFT_KEYWORDS -> { elementType in KtTokens.SOFT_KEYWORDS -> {
when (elementType) { when (elementType) {
@@ -49,6 +54,14 @@ internal class BeforeResolveHighlightingVisitor(private val holder: AnnotationHo
holder.createInfoAnnotation(element, null).textAttributes = attributes holder.createInfoAnnotation(element, null).textAttributes = attributes
} }
private fun willApplyRainbowHighlight(element: KDocLink): Boolean {
if (!RainbowHighlighter.isRainbowEnabledWithInheritance(EditorColorsManager.getInstance().globalScheme, KotlinLanguage.INSTANCE)) {
return false
}
// Can't use resolve because it will access indices
return (element.parent as? KDocTag)?.knownTag == KDocKnownTag.PARAM
}
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
if (ApplicationManager.getApplication().isUnitTestMode) return if (ApplicationManager.getApplication().isUnitTestMode) return
+2
View File
@@ -533,6 +533,8 @@
<annotator language="kotlin" implementationClass="org.jetbrains.kotlin.idea.highlighter.PlatformHeaderAnnotator"/> <annotator language="kotlin" implementationClass="org.jetbrains.kotlin.idea.highlighter.PlatformHeaderAnnotator"/>
<problemHighlightFilter implementation="org.jetbrains.kotlin.idea.highlighter.KotlinProblemHighlightFilter"/> <problemHighlightFilter implementation="org.jetbrains.kotlin.idea.highlighter.KotlinProblemHighlightFilter"/>
<highlightVisitor implementation="org.jetbrains.kotlin.idea.highlighter.KotlinRainbowVisitor"/>
<annotator language="JAVA" implementationClass="org.jetbrains.kotlin.idea.java.UnimplementedKotlinInterfaceMemberAnnotator"/> <annotator language="JAVA" implementationClass="org.jetbrains.kotlin.idea.java.UnimplementedKotlinInterfaceMemberAnnotator"/>
<extendWordSelectionHandler implementation="org.jetbrains.kotlin.idea.editor.wordSelection.KotlinStatementGroupSelectioner"/> <extendWordSelectionHandler implementation="org.jetbrains.kotlin.idea.editor.wordSelection.KotlinStatementGroupSelectioner"/>
@@ -22,13 +22,15 @@ import com.intellij.openapi.options.OptionsBundle
import com.intellij.openapi.options.colors.AttributesDescriptor import com.intellij.openapi.options.colors.AttributesDescriptor
import com.intellij.openapi.options.colors.ColorDescriptor import com.intellij.openapi.options.colors.ColorDescriptor
import com.intellij.openapi.options.colors.ColorSettingsPage import com.intellij.openapi.options.colors.ColorSettingsPage
import com.intellij.openapi.options.colors.RainbowColorSettingsPage
import org.jetbrains.kotlin.idea.KotlinBundle import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.KotlinIcons import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.KotlinLanguage
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.util.* import java.util.*
class KotlinColorSettingsPage : ColorSettingsPage { class KotlinColorSettingsPage : ColorSettingsPage, RainbowColorSettingsPage {
override fun getLanguage() = KotlinLanguage.INSTANCE
override fun getIcon() = KotlinIcons.SMALL_LOGO override fun getIcon() = KotlinIcons.SMALL_LOGO
override fun getHighlighter(): SyntaxHighlighter = KotlinHighlighter() override fun getHighlighter(): SyntaxHighlighter = KotlinHighlighter()
@@ -162,4 +164,9 @@ var <PACKAGE_PROPERTY><MUTABLE_VARIABLE>globalCounter</MUTABLE_VARIABLE></PACKAG
override fun getColorDescriptors(): Array<ColorDescriptor> = ColorDescriptor.EMPTY_ARRAY override fun getColorDescriptors(): Array<ColorDescriptor> = ColorDescriptor.EMPTY_ARRAY
override fun getDisplayName(): String = KotlinLanguage.NAME override fun getDisplayName(): String = KotlinLanguage.NAME
override fun isRainbowType(type: TextAttributesKey): Boolean {
return type == KotlinHighlightingColors.LOCAL_VARIABLE ||
type == KotlinHighlightingColors.PARAMETER
}
} }
@@ -0,0 +1,100 @@
/*
* Copyright 2010-2017 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.highlighter
import com.intellij.codeInsight.daemon.RainbowVisitor
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.descriptors.VariableDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.highlighter.KotlinHighlightingColors.*
import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
class KotlinRainbowVisitor : RainbowVisitor() {
override fun suitableForFile(file: PsiFile) = file is KtFile
override fun clone() = KotlinRainbowVisitor()
override fun visit(element: PsiElement) {
if (element.isRainbowDeclaration()) {
val rainbowElement = (element as KtNamedDeclaration).nameIdentifier ?: return
addRainbowHighlight(element, rainbowElement)
}
else if (element is KtSimpleNameExpression) {
val qualifiedExpression = PsiTreeUtil.getParentOfType(element, KtQualifiedExpression::class.java, true,
KtLambdaExpression::class.java, KtValueArgumentList::class.java)
if (qualifiedExpression?.selectorExpression?.isAncestor(element) == true) return
val bindingContext = element.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS)
val targets = element.getReferenceTargets(bindingContext)
val targetVariable = targets.firstIsInstanceOrNull<VariableDescriptor>()
if (targetVariable != null) {
val targetElement = DescriptorToSourceUtils.getSourceFromDescriptor(targetVariable)
if (targetElement.isRainbowDeclaration()) {
addRainbowHighlight(targetElement!!, element)
}
else if (targetElement == null && element.getReferencedName() == "it") {
addRainbowHighlight(element, element)
}
}
}
else if (element is KDocName) {
val target = element.reference?.resolve() ?: return
if (target.isRainbowDeclaration()) {
addRainbowHighlight(target, element, KDOC_LINK)
}
}
}
private fun addRainbowHighlight(target: PsiElement, rainbowElement: PsiElement,
attributesKey: TextAttributesKey? = null) {
val lambdaSequenceIterator = target.parents
.takeWhile { it !is KtDeclaration || it.isAnonymousFunction() || it is KtFunctionLiteral }
.filter { it is KtLambdaExpression || it.isAnonymousFunction() }
.iterator()
val attributesKeyToUse = attributesKey ?: (if (target is KtParameter) PARAMETER else LOCAL_VARIABLE)
if (lambdaSequenceIterator.hasNext()) {
var lambda = lambdaSequenceIterator.next()
var lambdaNestingLevel = 0
while (lambdaSequenceIterator.hasNext()) {
lambdaNestingLevel++
lambda = lambdaSequenceIterator.next()
}
addInfo(getInfo(lambda, rainbowElement, "$lambdaNestingLevel${rainbowElement.text}", attributesKeyToUse))
return
}
val context = target.getStrictParentOfType<KtDeclarationWithBody>() ?: return
addInfo(getInfo(context, rainbowElement, rainbowElement.text, attributesKeyToUse))
}
private fun PsiElement?.isRainbowDeclaration() =
(this is KtProperty && isLocal) || (this is KtParameter && getStrictParentOfType<KtPrimaryConstructor>() == null)
}
private fun PsiElement.isAnonymousFunction(): Boolean = this is KtNamedFunction && nameIdentifier == null
@@ -0,0 +1,79 @@
/*
* Copyright 2010-2017 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.highlighter
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
class KotlinRainbowHighlighterTest : KotlinLightCodeInsightFixtureTestCase() {
override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
fun testRainbowSimple() {
checkRainbow("""
fun main(<rainbow color='ff000003'>args</rainbow>: Array<String>) {
val <rainbow color='ff000004'>local1</rainbow> = ""
println(<rainbow color='ff000004'>local1</rainbow> + <rainbow color='ff000003'>args</rainbow>)
}
""")
}
fun testRainbowNestedIt() {
checkRainbow("""
fun main(<rainbow color='ff000003'>args</rainbow>: Array<String>) {
listOf("abc", "def").filter { <rainbow color='ff000002'>it</rainbow>.any { <rainbow color='ff000003'>it</rainbow> == 'a' } }
}
""")
}
fun testRainbowNestedAnonymousFunction() {
checkRainbow("""
fun main() {
listOf("abc", "def").filter(fun(<rainbow color='ff000002'>it</rainbow>): Boolean {
return <rainbow color='ff000002'>it</rainbow>.any(fun(<rainbow color='ff000003'>it</rainbow>): Boolean {
return <rainbow color='ff000003'>it</rainbow> == 'a'
})
})
}
""")
}
fun testKDoc() {
checkRainbow("""
/**
* @param <rainbow color='ff000003'>args</rainbow> foo
*/
fun main(<rainbow color='ff000003'>args</rainbow>: Array<String>) {
}
""")
}
fun testQualified() {
checkRainbow("""
data class Foo(val bar: String)
fun main(<rainbow color='ff000004'>foo</rainbow>: Foo) {
println(<rainbow color='ff000004'>foo</rainbow>.bar)
System.out.println(<rainbow color='ff000004'>foo</rainbow>)
}
""")
}
private fun checkRainbow(code: String) {
myFixture.testRainbow("rainbow.kt", code, true, true)
}
}