Add initial support for injecting languages with comment or annotation

#KT-11472 Fixed
This commit is contained in:
Nikolay Krasko
2016-03-25 14:55:15 +03:00
parent 4819c07c95
commit b4f5f0e10e
4 changed files with 578 additions and 0 deletions
+6
View File
@@ -1433,6 +1433,12 @@
<filetype.decompiler filetype="kotlin_builtins" implementationClass="com.intellij.psi.impl.compiled.ClassFileDecompiler"/>
<pathMacroExpandableProtocol protocol="kotlin-js-meta"/>
<languageInjector implementation="org.jetbrains.kotlin.idea.injection.KotlinLanguageInjector" />
</extensions>
<extensions defaultExtensionNs="org.intellij.intelliLang">
<languageSupport implementation="org.jetbrains.kotlin.idea.injection.KotlinLanguageInjectionSupport"/>
</extensions>
<xi:include href="extensions/ide.xml" xpointer="xpointer(/idea-plugin/*)"/>
@@ -0,0 +1,243 @@
/*
* Copyright 2010-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.kotlin.idea.injection
import com.intellij.codeInsight.AnnotationUtil
import com.intellij.lang.Language
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiTreeUtil.getDeepestLast
import com.intellij.util.ArrayUtil
import com.intellij.util.Processor
import org.intellij.plugins.intelliLang.Configuration
import org.intellij.plugins.intelliLang.inject.AbstractLanguageInjectionSupport
import org.intellij.plugins.intelliLang.inject.InjectLanguageAction
import org.intellij.plugins.intelliLang.inject.InjectedLanguage
import org.intellij.plugins.intelliLang.inject.TemporaryPlacesRegistry
import org.jetbrains.annotations.NonNls
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.findAnnotation
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.psi.psiUtil.startOffset
@NonNls val KOTLIN_SUPPORT_ID = "kotlin"
val LINE_LANGUAGE_REGEXP_COMMENT = Regex("//\\s*language=[\\w-]+")
class KotlinLanguageInjectionSupport : AbstractLanguageInjectionSupport() {
override fun getId(): String = KOTLIN_SUPPORT_ID
override fun getPatternClasses() = ArrayUtil.EMPTY_CLASS_ARRAY
override fun isApplicableTo(host: PsiLanguageInjectionHost?) = host is KtElement
override fun addInjectionInPlace(language: Language?, host: PsiLanguageInjectionHost?): Boolean {
if (language == null || host == null) return false
val configuration = Configuration.getProjectInstance(host.project).advancedConfiguration
if (!configuration.isSourceModificationAllowed) {
// It's not allowed to modify code without explicit permission. Postpone adding @Inject or comment till it granted.
host.putUserData(InjectLanguageAction.FIX_KEY, Processor { host -> addInjectionInstructionInCode(language, host) })
return false
}
if (!addInjectionInstructionInCode(language, host)) {
return false
}
TemporaryPlacesRegistry.getInstance(host.project).addHostWithUndo(host, InjectedLanguage.create(language.id))
return true
}
override fun removeInjectionInPlace(psiElement: PsiLanguageInjectionHost?): Boolean {
if (psiElement == null || psiElement !is KtElement) return false
val project = psiElement.getProject()
val injectionAnnotation = findAnnotationInjection(psiElement)
val injectionComment = findInjectionComment(psiElement)
val injectInstructions = listOf(injectionAnnotation, injectionComment).filterNotNull()
val configuration = Configuration.getProjectInstance(project)
TemporaryPlacesRegistry.getInstance(project).removeHostWithUndo(project, psiElement)
configuration.replaceInjectionsWithUndo(project, listOf(), listOf(), injectInstructions)
return true
}
fun findInjectionComment(host: KtElement): PsiComment? {
if (findCommentInjection(host, null) == null) return null
val commentsToCheck: Sequence<PsiComment> =
findElementToInjectWithAnnotation(host)?.findChildrenComments() ?:
findElementToInjectWithComment(host)?.findCommentsBeforeElement() ?:
return null
return commentsToCheck.firstOrNull {
it.node.elementType == KtTokens.EOL_COMMENT && it.text.matches(LINE_LANGUAGE_REGEXP_COMMENT)
}
}
fun findAnnotationInjectionLanguageId(host: KtElement): String? = doFindAnnotationInjectionLanguageId(host)
}
private fun doFindAnnotationInjectionLanguageId(host: KtElement): String? {
val annotationEntry = findAnnotationInjection(host) ?: return null
val firstArgument: ValueArgument = annotationEntry.valueArguments.firstOrNull() ?: return null
val firstStringArgument = firstArgument.getArgumentExpression() as? KtStringTemplateExpression ?: return null
val firstStringEntry = firstStringArgument.entries.singleOrNull() ?: return null
return firstStringEntry.text
}
private fun findAnnotationInjection(host: KtElement): KtAnnotationEntry? {
val modifierListOwner = findElementToInjectWithAnnotation(host) ?: return null
val modifierList = modifierListOwner.modifierList ?: return null
// Host can't be before annotation
if (host.startOffset < modifierList.endOffset) return null
return modifierListOwner.findAnnotation(FqName(AnnotationUtil.LANGUAGE))
}
private fun canInjectWithAnnotation(host: PsiElement): Boolean {
val module = ModuleUtilCore.findModuleForPsiElement(host) ?: return false
val javaPsiFacade = JavaPsiFacade.getInstance(module.project)
return javaPsiFacade.findClass(AnnotationUtil.LANGUAGE, GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module)) != null
}
private fun findElementToInjectWithAnnotation(host: KtElement): KtModifierListOwner? {
return PsiTreeUtil.getParentOfType(
host,
KtModifierListOwner::class.java,
false, /* strict */
KtBlockExpression::class.java, KtParameterList::class.java, KtTypeParameterList::class.java /* Stop at */
)
}
private fun findElementToInjectWithComment(host: KtElement): KtExpression? {
val parentBlockExpression = PsiTreeUtil.getParentOfType(
host,
KtBlockExpression::class.java,
true, /* strict */
KtDeclaration::class.java /* Stop at */
) ?: return null
return parentBlockExpression.statements.firstOrNull { statement ->
PsiTreeUtil.isAncestor(statement, host, false) && checkIsValidPlaceForInjectionWithLineComment(statement, host)
}
}
private fun addInjectionInstructionInCode(language: Language, host: PsiLanguageInjectionHost): Boolean {
val ktHost = host as? KtElement ?: return false
val project = ktHost.project
// Find the place where injection can be stated with annotation or comment
val modifierListOwner = findElementToInjectWithAnnotation(ktHost)
if (modifierListOwner != null && canInjectWithAnnotation(ktHost)) {
project.executeWriteCommand("Add injection annotation") {
modifierListOwner.addAnnotation(FqName(AnnotationUtil.LANGUAGE), "\"${language.id}\"")
}
return true
}
// Find the place where injection can be done with one-line comment
val commentBeforeAnchor: PsiElement =
modifierListOwner?.firstNonCommentChild() ?:
findElementToInjectWithComment(ktHost) ?:
return false
val psiFactory = KtPsiFactory(project)
val injectComment = psiFactory.createComment("//language=" + language.id)
project.executeWriteCommand("Add injection comment") {
commentBeforeAnchor.parent.addBefore(injectComment, commentBeforeAnchor)
}
return true
}
// Inspired with InjectorUtils.findCommentInjection()
private fun checkIsValidPlaceForInjectionWithLineComment(statement: KtExpression, host: KtElement): Boolean {
// make sure comment is close enough and ...
val statementStartOffset = statement.startOffset
val hostStart = host.startOffset
if (hostStart < statementStartOffset || hostStart - statementStartOffset > 120) {
return false
}
if (hostStart - statementStartOffset > 2) {
// ... there's no non-empty valid host in between comment and e2
if (prevWalker(host, statement).asSequence().takeWhile { it != null }.any {
it is PsiLanguageInjectionHost && it.isValidHost && !StringUtil.isEmptyOrSpaces(it.text)
}) {
return false
}
}
return true
}
private fun PsiElement.findChildrenComments(): Sequence<PsiComment> {
return firstChild.siblings().takeWhile { it is PsiComment || it is PsiWhiteSpace }.filterIsInstance<PsiComment>()
}
private fun PsiElement.findCommentsBeforeElement(): Sequence<PsiComment> {
return siblings(forward = false, withItself = false).takeWhile { it is PsiComment || it is PsiWhiteSpace }.filterIsInstance<PsiComment>()
}
private fun PsiElement.firstNonCommentChild(): PsiElement? {
return firstChild.siblings().dropWhile { it is PsiComment || it is PsiWhiteSpace }.firstOrNull()
}
// Based on InjectorUtils.prevWalker
private fun prevWalker(element: PsiElement, scope: PsiElement): Iterator<PsiElement?> {
return object : Iterator<PsiElement?> {
private var e: PsiElement? = element
override fun hasNext(): Boolean = true
override fun next(): PsiElement? {
val current = e
if (current == null || current === scope) return null
val prev = current.prevSibling
if (prev != null) {
e = getDeepestLast(prev)
return e
}
else {
val parent = current.parent
e = if (parent === scope || parent is PsiFile) null else parent
return e
}
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-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.kotlin.idea.injection
import com.intellij.openapi.util.TextRange
import com.intellij.psi.InjectedLanguagePlaces
import com.intellij.psi.LanguageInjector
import com.intellij.psi.PsiLanguageInjectionHost
import org.intellij.plugins.intelliLang.inject.InjectorUtils
import org.jetbrains.kotlin.psi.KtElement
import java.util.*
class KotlinLanguageInjector : LanguageInjector {
val kotlinSupport: KotlinLanguageInjectionSupport? by lazy {
ArrayList(InjectorUtils.getActiveInjectionSupports()).filterIsInstance(KotlinLanguageInjectionSupport::class.java).firstOrNull()
}
override fun getLanguagesToInject(host: PsiLanguageInjectionHost, injectionPlacesRegistrar: InjectedLanguagePlaces) {
if (host !is KtElement) return
if (!host.isValidHost) return
val support = kotlinSupport ?: return
val languageId = support.findAnnotationInjectionLanguageId(host) ?: return
val language = InjectorUtils.getLanguageByString(languageId) ?: return
injectionPlacesRegistrar.addPlace(language, TextRange.from(0, host.getTextLength()), null, null)
}
}
@@ -0,0 +1,286 @@
/*
* Copyright 2010-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.kotlin.psi
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.ContentEntry
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference
import com.intellij.psi.injection.Injectable
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor
import junit.framework.TestCase
import org.intellij.plugins.intelliLang.Configuration
import org.intellij.plugins.intelliLang.inject.InjectLanguageAction
import org.intellij.plugins.intelliLang.inject.UnInjectLanguageAction
import org.intellij.plugins.intelliLang.references.FileReferenceInjector
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import java.io.File
class KotlinInjectionTest : KotlinLightCodeInsightFixtureTestCase() {
fun testInjectUnInjectOnSimpleString() {
myFixture.configureByText("test.kt",
"""val test = "<caret>simple" """)
TestCase.assertTrue(InjectLanguageAction().isAvailable(project, myFixture.editor, myFixture.file))
TestCase.assertFalse(UnInjectLanguageAction().isAvailable(project, myFixture.editor, myFixture.file))
InjectLanguageAction.invokeImpl(project, myFixture.editor, myFixture.file, FileReferenceInjector())
TestCase.assertTrue(myFixture.getReferenceAtCaretPosition() is FileReference)
TestCase.assertFalse(InjectLanguageAction().isAvailable(project, myFixture.editor, myFixture.file))
TestCase.assertTrue(UnInjectLanguageAction().isAvailable(project, myFixture.editor, myFixture.file))
UnInjectLanguageAction.invokeImpl(project, myFixture.editor, myFixture.file)
TestCase.assertNull(myFixture.getReferenceAtCaretPosition())
}
fun testInjectionWithCommentOnProperty() = doTestInjection(
"""
|//language=file-reference
|val test = "<caret>simple"
""")
fun testInjectionWithMultipleCommentsOnFun() = doTestInjection(
"""
|// Some comment
|// Other comment
|//language=file-reference
|fun test() = "<caret>simple"
""")
fun testInjectionWithAnnotationOnPropertyWithAnnotation() = doTestInjection(
"""
|@org.intellij.lang.annotations.Language("file-reference")
|val test = "<caret>simple"
""")
fun testInjectWithCommentOnProperty() = doFileReferenceInjectTest(
"""
|val test = "<caret>simple"
""",
"""
|//language=file-reference
|val test = "simple"
"""
)
fun testInjectWithCommentOnCommentedProperty() = doFileReferenceInjectTest(
"""
|// Hello
|val test = "<caret>simple"
""",
"""
|// Hello
|//language=file-reference
|val test = "simple"
"""
)
fun testInjectWithCommentOnPropertyWithKDoc() = doFileReferenceInjectTest(
"""
|/**
| * Hi
| */
|val test = "<caret>simple"
""",
"""
|/**
| * Hi
| */
|//language=file-reference
|val test = "<caret>simple"
"""
)
fun testInjectWithCommentOnExpression() = doFileReferenceInjectTest(
"""
|fun test() {
| "<caret>"
|}
""",
"""
|fun test() {
| //language=file-reference
| "<caret>"
|}
"""
)
fun testInjectWithCommentOnDeepExpression() = doFileReferenceInjectTest(
"""
|fun test() {
| "" + "<caret>"
|}
""",
"""
|fun test() {
| "" + "<caret>"
|}
"""
)
fun testInjectOnPropertyWithAnnotation() = doFileReferenceInjectTest(
"""
|val test = "<caret>simple"
""",
"""
|import org.intellij.lang.annotations.Language
|
|@Language("file-reference")
|val test = "simple"
"""
)
fun testInjectWithOnExpressionWithAnnotation() = doFileReferenceInjectTest(
"""
|fun test() {
| "<caret>"
|}
""",
"""
|fun test() {
| //language=file-reference
| "<caret>"
|}
"""
)
// TODO: add test for non-default language annotation
fun testRemoveInjectionWithAnnotation() = doRemoveInjectionTest(
"""
|import org.intellij.lang.annotations.Language
|
|@Language("file-reference")
|val test = "<caret>simple"
""",
"""
|import org.intellij.lang.annotations.Language
|
|val test = "simple"
"""
)
fun testRemoveInjectionWithComment() = doRemoveInjectionTest(
"""
|//language=file-reference
|val test = "<caret>simple"
""",
"""
|val test = "simple"
"""
)
fun testRemoveInjectionWithCommentNotFirst() = doRemoveInjectionTest(
"""
|// Some comment. To do a language injection, add a line comment language=some instruction.
|// language=file-reference
|val test = "<caret>simple"
""",
"""
|// Some comment. To do a language injection, add a line comment language=some instruction.
|val test = "simple"
"""
)
fun testRemoveInjectionWithCommentAfterKDoc() = doRemoveInjectionTest(
"""
|/**Property*/
|// language=file-reference
|val test = "<caret>simple"
""",
"""
|/**Property*/
|val test = "simple"
"""
)
fun testRemoveInjectionWithCommentInExpression() = doRemoveInjectionTest(
"""
|fun test() {
| // This is my favorite part
| // language=RegExp
| "<caret>something"
|}
""",
"""
|fun test() {
| // This is my favorite part
| "something"
|}
"""
)
override fun getProjectDescriptor(): LightProjectDescriptor {
if (getTestName(true).endsWith("WithAnnotation")) {
return PROJECT_DESCRIPTOR_WITH_LANGUAGE_ANNOTATION_LIB
}
return JAVA_LATEST
}
private fun doTestInjection(text: String) {
myFixture.configureByText("${getTestName(true)}.kt", text.trimMargin())
TestCase.assertFalse(InjectLanguageAction().isAvailable(project, myFixture.editor, myFixture.file))
TestCase.assertTrue(UnInjectLanguageAction().isAvailable(project, myFixture.editor, myFixture.file))
}
private fun doRemoveInjectionTest(before: String, after: String) {
myFixture.setCaresAboutInjection(false)
myFixture.configureByText("${getTestName(true)}.kt", before.trimMargin())
TestCase.assertTrue(UnInjectLanguageAction().isAvailable(project, myFixture.editor, myFixture.file))
UnInjectLanguageAction.invokeImpl(project, myFixture.editor, myFixture.file)
myFixture.checkResult(after.trimMargin())
}
private fun doFileReferenceInjectTest(before: String, after: String) {
doTest(FileReferenceInjector(), before, after)
}
private fun doTest(injectable: Injectable, before: String, after: String) {
val configuration = Configuration.getProjectInstance(project).advancedConfiguration
val allowed = configuration.isSourceModificationAllowed
configuration.isSourceModificationAllowed = true
try {
myFixture.configureByText("${getTestName(true)}.kt", before.trimMargin())
InjectLanguageAction.invokeImpl(project, myFixture.editor, myFixture.file, injectable)
myFixture.checkResult(after.trimMargin())
}
finally {
configuration.isSourceModificationAllowed = allowed
}
}
}
private val PROJECT_DESCRIPTOR_WITH_LANGUAGE_ANNOTATION_LIB = object : DefaultLightProjectDescriptor() {
override fun configureModule(module: Module, model: ModifiableRootModel, contentEntry: ContentEntry) {
super.configureModule(module, model, contentEntry)
val jarUrl = VfsUtil.getUrlForLibraryRoot(File("ideaSDK/lib/annotations.jar"))
val libraryModel = model.moduleLibraryTable.modifiableModel.createLibrary("annotations").modifiableModel
libraryModel.addRoot(jarUrl, OrderRootType.CLASSES)
libraryModel.commit()
}
}