From 5315f4d9da3dd56ebb7b7fedc66895969e0c7a2e Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Fri, 21 Apr 2017 18:34:19 +0200 Subject: [PATCH] Initial implementation of rainbow highlighting for Kotlin #KT-12629 Fixed --- .../BeforeResolveHighlightingVisitor.kt | 15 ++- idea/src/META-INF/plugin.xml | 2 + .../highlighter/KotlinColorSettingsPage.kt | 9 +- .../idea/highlighter/KotlinRainbowVisitor.kt | 100 ++++++++++++++++++ .../KotlinRainbowHighlighterTest.kt | 79 ++++++++++++++ 5 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowVisitor.kt create mode 100644 idea/tests/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowHighlighterTest.kt diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/BeforeResolveHighlightingVisitor.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/BeforeResolveHighlightingVisitor.kt index af238d889bc..7536b4b51a9 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/BeforeResolveHighlightingVisitor.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/BeforeResolveHighlightingVisitor.kt @@ -16,11 +16,16 @@ package org.jetbrains.kotlin.idea.highlighter +import com.intellij.codeHighlighting.RainbowHighlighter import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.TextRange 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.KDocTag import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtExpressionWithLabel import org.jetbrains.kotlin.psi.KtLambdaExpression @@ -34,7 +39,7 @@ internal class BeforeResolveHighlightingVisitor(private val holder: AnnotationHo override fun visitElement(element: PsiElement) { val elementType = element.node.elementType val attributes = when { - element is KDocLink -> KotlinHighlightingColors.KDOC_LINK + element is KDocLink && !willApplyRainbowHighlight(element) -> KotlinHighlightingColors.KDOC_LINK elementType in KtTokens.SOFT_KEYWORDS -> { when (elementType) { @@ -49,6 +54,14 @@ internal class BeforeResolveHighlightingVisitor(private val holder: AnnotationHo 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) { if (ApplicationManager.getApplication().isUnitTestMode) return diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 80c88cb3adc..5256681c30e 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -533,6 +533,8 @@ + + diff --git a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinColorSettingsPage.kt b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinColorSettingsPage.kt index 3715e121076..355df31574c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinColorSettingsPage.kt +++ b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinColorSettingsPage.kt @@ -22,13 +22,15 @@ import com.intellij.openapi.options.OptionsBundle import com.intellij.openapi.options.colors.AttributesDescriptor import com.intellij.openapi.options.colors.ColorDescriptor 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.KotlinIcons import org.jetbrains.kotlin.idea.KotlinLanguage import java.lang.reflect.Modifier import java.util.* -class KotlinColorSettingsPage : ColorSettingsPage { +class KotlinColorSettingsPage : ColorSettingsPage, RainbowColorSettingsPage { + override fun getLanguage() = KotlinLanguage.INSTANCE override fun getIcon() = KotlinIcons.SMALL_LOGO override fun getHighlighter(): SyntaxHighlighter = KotlinHighlighter() @@ -162,4 +164,9 @@ var globalCounter = ColorDescriptor.EMPTY_ARRAY override fun getDisplayName(): String = KotlinLanguage.NAME + + override fun isRainbowType(type: TextAttributesKey): Boolean { + return type == KotlinHighlightingColors.LOCAL_VARIABLE || + type == KotlinHighlightingColors.PARAMETER + } } diff --git a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowVisitor.kt b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowVisitor.kt new file mode 100644 index 00000000000..08a94ad1659 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowVisitor.kt @@ -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() + 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() ?: return + addInfo(getInfo(context, rainbowElement, rainbowElement.text, attributesKeyToUse)) + } + + private fun PsiElement?.isRainbowDeclaration() = + (this is KtProperty && isLocal) || (this is KtParameter && getStrictParentOfType() == null) +} + +private fun PsiElement.isAnonymousFunction(): Boolean = this is KtNamedFunction && nameIdentifier == null diff --git a/idea/tests/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowHighlighterTest.kt b/idea/tests/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowHighlighterTest.kt new file mode 100644 index 00000000000..823e9fed158 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/highlighter/KotlinRainbowHighlighterTest.kt @@ -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(args: Array) { + val local1 = "" + println(local1 + args) + } + """) + } + + fun testRainbowNestedIt() { + checkRainbow(""" + fun main(args: Array) { + listOf("abc", "def").filter { it.any { it == 'a' } } + } + """) + } + + fun testRainbowNestedAnonymousFunction() { + checkRainbow(""" + fun main() { + listOf("abc", "def").filter(fun(it): Boolean { + return it.any(fun(it): Boolean { + return it == 'a' + }) + }) + } + """) + } + + fun testKDoc() { + checkRainbow(""" + /** + * @param args foo + */ + fun main(args: Array) { + } + """) + } + + fun testQualified() { + checkRainbow(""" + data class Foo(val bar: String) + + fun main(foo: Foo) { + println(foo.bar) + System.out.println(foo) + } + """) + + } + + private fun checkRainbow(code: String) { + myFixture.testRainbow("rainbow.kt", code, true, true) + } +}