diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index 0c92e38ff6a..fd458449854 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1433,6 +1433,12 @@
+
+
+
+
+
+
diff --git a/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt
new file mode 100644
index 00000000000..b06e9418409
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt
@@ -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 =
+ 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 {
+ return firstChild.siblings().takeWhile { it is PsiComment || it is PsiWhiteSpace }.filterIsInstance()
+}
+
+private fun PsiElement.findCommentsBeforeElement(): Sequence {
+ return siblings(forward = false, withItself = false).takeWhile { it is PsiComment || it is PsiWhiteSpace }.filterIsInstance()
+}
+
+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 {
+ return object : Iterator {
+ 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
+ }
+ }
+ }
+}
diff --git a/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt
new file mode 100644
index 00000000000..22e4ac2e301
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt
new file mode 100644
index 00000000000..d0813397d77
--- /dev/null
+++ b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt
@@ -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 = "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 = "simple"
+ """)
+
+ fun testInjectionWithMultipleCommentsOnFun() = doTestInjection(
+ """
+ |// Some comment
+ |// Other comment
+ |//language=file-reference
+ |fun test() = "simple"
+ """)
+
+ fun testInjectionWithAnnotationOnPropertyWithAnnotation() = doTestInjection(
+ """
+ |@org.intellij.lang.annotations.Language("file-reference")
+ |val test = "simple"
+ """)
+
+ fun testInjectWithCommentOnProperty() = doFileReferenceInjectTest(
+ """
+ |val test = "simple"
+ """,
+ """
+ |//language=file-reference
+ |val test = "simple"
+ """
+ )
+
+ fun testInjectWithCommentOnCommentedProperty() = doFileReferenceInjectTest(
+ """
+ |// Hello
+ |val test = "simple"
+ """,
+ """
+ |// Hello
+ |//language=file-reference
+ |val test = "simple"
+ """
+ )
+
+ fun testInjectWithCommentOnPropertyWithKDoc() = doFileReferenceInjectTest(
+ """
+ |/**
+ | * Hi
+ | */
+ |val test = "simple"
+ """,
+ """
+ |/**
+ | * Hi
+ | */
+ |//language=file-reference
+ |val test = "simple"
+ """
+ )
+
+ fun testInjectWithCommentOnExpression() = doFileReferenceInjectTest(
+ """
+ |fun test() {
+ | ""
+ |}
+ """,
+ """
+ |fun test() {
+ | //language=file-reference
+ | ""
+ |}
+ """
+ )
+
+ fun testInjectWithCommentOnDeepExpression() = doFileReferenceInjectTest(
+ """
+ |fun test() {
+ | "" + ""
+ |}
+ """,
+ """
+ |fun test() {
+ | "" + ""
+ |}
+ """
+ )
+
+ fun testInjectOnPropertyWithAnnotation() = doFileReferenceInjectTest(
+ """
+ |val test = "simple"
+ """,
+ """
+ |import org.intellij.lang.annotations.Language
+ |
+ |@Language("file-reference")
+ |val test = "simple"
+ """
+ )
+
+ fun testInjectWithOnExpressionWithAnnotation() = doFileReferenceInjectTest(
+ """
+ |fun test() {
+ | ""
+ |}
+ """,
+ """
+ |fun test() {
+ | //language=file-reference
+ | ""
+ |}
+ """
+ )
+
+ // TODO: add test for non-default language annotation
+
+ fun testRemoveInjectionWithAnnotation() = doRemoveInjectionTest(
+ """
+ |import org.intellij.lang.annotations.Language
+ |
+ |@Language("file-reference")
+ |val test = "simple"
+ """,
+ """
+ |import org.intellij.lang.annotations.Language
+ |
+ |val test = "simple"
+ """
+ )
+
+ fun testRemoveInjectionWithComment() = doRemoveInjectionTest(
+ """
+ |//language=file-reference
+ |val test = "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 = "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 = "simple"
+ """,
+ """
+ |/**Property*/
+ |val test = "simple"
+ """
+ )
+
+ fun testRemoveInjectionWithCommentInExpression() = doRemoveInjectionTest(
+ """
+ |fun test() {
+ | // This is my favorite part
+ | // language=RegExp
+ | "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()
+ }
+}