diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt.172 b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt.172 new file mode 100644 index 00000000000..44882dc742e --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt.172 @@ -0,0 +1,353 @@ +/* + * 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.injection.MultiHostInjector +import com.intellij.lang.injection.MultiHostRegistrar +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.psi.* +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil +import com.intellij.psi.search.LocalSearchScope +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.PsiTreeUtil +import org.intellij.plugins.intelliLang.Configuration +import org.intellij.plugins.intelliLang.inject.InjectorUtils +import org.intellij.plugins.intelliLang.inject.LanguageInjectionSupport +import org.intellij.plugins.intelliLang.inject.TemporaryPlacesRegistry +import org.intellij.plugins.intelliLang.inject.config.BaseInjection +import org.intellij.plugins.intelliLang.inject.java.JavaLanguageInjectionSupport +import org.intellij.plugins.intelliLang.util.AnnotationUtilEx +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.references.KtReference +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.idea.runInReadActionWithWriteActionPriority +import org.jetbrains.kotlin.idea.util.ProjectRootsUtil +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.annotations.argumentValue +import org.jetbrains.kotlin.resolve.constants.StringValue +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import java.util.* +import kotlin.collections.ArrayList +import org.jetbrains.kotlin.util.aliasImportMap +import org.jetbrains.kotlin.utils.SmartList +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +class KotlinLanguageInjector( + val configuration: Configuration, + val project: Project, + val temporaryPlacesRegistry: TemporaryPlacesRegistry +) : MultiHostInjector { + companion object { + private val STRING_LITERALS_REGEXP = "\"([^\"]*)\"".toRegex() + private val ABSENT_KOTLIN_INJECTION = BaseInjection("ABSENT_KOTLIN_BASE_INJECTION") + } + + val kotlinSupport: KotlinLanguageInjectionSupport? by lazy { + ArrayList(InjectorUtils.getActiveInjectionSupports()).filterIsInstance(KotlinLanguageInjectionSupport::class.java).firstOrNull() + } + + private data class KotlinCachedInjection(val modificationCount: Long, val baseInjection: BaseInjection) + private var KtStringTemplateExpression.cachedInjectionWithModification: KotlinCachedInjection? by UserDataProperty( + Key.create("CACHED_INJECTION_WITH_MODIFICATION")) + + override fun getLanguagesToInject(registrar: MultiHostRegistrar, context: PsiElement) { + val ktHost: KtStringTemplateExpression = context as? KtStringTemplateExpression ?: return + if (!context.isValidHost) return + + val support = kotlinSupport ?: return + + if (!ProjectRootsUtil.isInProjectOrLibSource(ktHost)) return + + val needImmediateAnswer = with(ApplicationManager.getApplication()) { isDispatchThread && !isUnitTestMode } + val kotlinCachedInjection = ktHost.cachedInjectionWithModification + val modificationCount = PsiManager.getInstance(project).modificationTracker.modificationCount + + val baseInjection = when { + needImmediateAnswer -> { + // Can't afford long counting or typing will be laggy. Force cache reuse even if it's outdated. + kotlinCachedInjection?.baseInjection ?: ABSENT_KOTLIN_INJECTION + } + kotlinCachedInjection != null && (modificationCount == kotlinCachedInjection.modificationCount) -> + // Cache is up-to-date + kotlinCachedInjection.baseInjection + else -> { + fun computeAndCache(): BaseInjection { + val computedInjection = computeBaseInjection(ktHost, support, registrar) ?: ABSENT_KOTLIN_INJECTION + ktHost.cachedInjectionWithModification = KotlinCachedInjection(modificationCount, computedInjection) + return computedInjection + } + + if (ApplicationManager.getApplication().isReadAccessAllowed && ProgressManager.getInstance().progressIndicator == null) { + // The action cannot be canceled by caller and by internal checkCanceled() calls. + // Force creating new indicator that is canceled on write action start, otherwise there might be lags in typing. + runInReadActionWithWriteActionPriority(::computeAndCache) ?: kotlinCachedInjection?.baseInjection ?: ABSENT_KOTLIN_INJECTION + } + else { + computeAndCache() + } + } + } + + if (baseInjection == ABSENT_KOTLIN_INJECTION) { + return + } + + val language = InjectorUtils.getLanguageByString(baseInjection.injectedLanguageId) ?: return + + if (ktHost.hasInterpolation()) { + val file = ktHost.containingKtFile + val parts = splitLiteralToInjectionParts(baseInjection, ktHost) ?: return + + if (parts.ranges.isEmpty()) return + + InjectorUtils.registerInjection(language, parts.ranges, file, registrar) + InjectorUtils.registerSupport(support, false, registrar) + InjectorUtils.putInjectedFileUserData(registrar, InjectedLanguageUtil.FRANKENSTEIN_INJECTION, + if (parts.isUnparsable) java.lang.Boolean.TRUE else null) + } + else { + InjectorUtils.registerInjectionSimple(ktHost, baseInjection, support, registrar) + } + } + + @Suppress("FoldInitializerAndIfToElvis") + private fun computeBaseInjection( + ktHost: KtStringTemplateExpression, + support: KotlinLanguageInjectionSupport, + registrar: MultiHostRegistrar): BaseInjection? { + val containingFile = ktHost.containingFile + + val tempInjectedLanguage = temporaryPlacesRegistry.getLanguageFor(ktHost, containingFile) + if (tempInjectedLanguage != null) { + InjectorUtils.putInjectedFileUserData(registrar, LanguageInjectionSupport.TEMPORARY_INJECTED_LANGUAGE, tempInjectedLanguage) + return BaseInjection(support.id).apply { + injectedLanguageId = tempInjectedLanguage.id + prefix = tempInjectedLanguage.prefix + suffix = tempInjectedLanguage.suffix + } + } + + return findInjectionInfo(ktHost)?.toBaseInjection(support) + } + + override fun elementsToInjectIn(): List> { + return listOf(KtStringTemplateExpression::class.java) + } + + private fun findInjectionInfo(place: KtElement, originalHost: Boolean = true): InjectionInfo? { + return injectWithExplicitCodeInstruction(place) + ?: injectWithCall(place) + ?: injectWithReceiver(place) + ?: injectWithVariableUsage(place, originalHost) + ?: injectWithAnnotationEntry(place) + } + + private fun injectWithExplicitCodeInstruction(host: KtElement): InjectionInfo? { + val support = kotlinSupport ?: return null + return InjectionInfo.fromBaseInjection(support.findCommentInjection(host)) ?: support.findAnnotationInjectionLanguageId(host) + } + + private fun injectWithReceiver(host: KtElement): InjectionInfo? { + val qualifiedExpression = host.parent as? KtDotQualifiedExpression ?: return null + if (qualifiedExpression.receiverExpression != host) return null + + val callExpression = qualifiedExpression.selectorExpression as? KtCallExpression ?: return null + val callee = callExpression.calleeExpression ?: return null + + if (isAnalyzeOff(qualifiedExpression.project)) return null + + val kotlinInjections = Configuration.getInstance().getInjections(KOTLIN_SUPPORT_ID) + + val calleeName = callee.text + val possibleNames = collectPossibleNames(kotlinInjections) + + if (calleeName !in possibleNames) { + return null + } + + for (reference in callee.references) { + ProgressManager.checkCanceled() + + val resolvedTo = reference.resolve() + if (resolvedTo is KtFunction) { + val injectionInfo = findInjection(resolvedTo.receiverTypeReference, kotlinInjections) + if (injectionInfo != null) { + return injectionInfo + } + } + } + + return null + } + + private fun collectPossibleNames(injections: List): Set { + val result = HashSet() + + for (injection in injections) { + val injectionPlaces = injection.injectionPlaces + for (place in injectionPlaces) { + val placeStr = place.toString() + val literals = STRING_LITERALS_REGEXP.findAll(placeStr).map { it.groupValues[1] } + result.addAll(literals) + } + } + + return result + } + + private fun injectWithVariableUsage(host: KtElement, originalHost: Boolean): InjectionInfo? { + // Given place is not original host of the injection so we stop to prevent stepping through indirect references + if (!originalHost) return null + + val ktHost: KtElement = host + val ktProperty = host.parent as? KtProperty ?: return null + if (ktProperty.initializer != host) return null + + if (isAnalyzeOff(ktHost.project)) return null + + val searchScope = LocalSearchScope(arrayOf(ktProperty.containingFile), "", true) + return ReferencesSearch.search(ktProperty, searchScope).asSequence().mapNotNull { psiReference -> + val element = psiReference.element as? KtElement ?: return@mapNotNull null + findInjectionInfo(element, false) + }.firstOrNull() + } + + private fun injectWithCall(host: KtElement): InjectionInfo? { + val ktHost: KtElement = host + val argument = ktHost.parent as? KtValueArgument ?: return null + + val callExpression = PsiTreeUtil.getParentOfType(ktHost, KtCallExpression::class.java) ?: return null + val callee = callExpression.calleeExpression ?: return null + + if (isAnalyzeOff(ktHost.project)) return null + + for (reference in callee.references) { + ProgressManager.checkCanceled() + + val resolvedTo = reference.resolve() + if (resolvedTo is PsiMethod) { + val injectionForJavaMethod = injectionForJavaMethod(argument, resolvedTo) + if (injectionForJavaMethod != null) { + return injectionForJavaMethod + } + } + else if (resolvedTo is KtFunction) { + val injectionForJavaMethod = injectionForKotlinCall(argument, resolvedTo, reference) + if (injectionForJavaMethod != null) { + return injectionForJavaMethod + } + } + } + + return null + } + + private fun injectionForJavaMethod(argument: KtValueArgument, javaMethod: PsiMethod): InjectionInfo? { + val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) + val psiParameter = javaMethod.parameterList.parameters.getOrNull(argumentIndex) ?: return null + + val injectionInfo = findInjection(psiParameter, Configuration.getInstance().getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) + if (injectionInfo != null) { + return injectionInfo + } + + val annotations = AnnotationUtilEx.getAnnotationFrom( + psiParameter, + Configuration.getProjectInstance(psiParameter.project).advancedConfiguration.languageAnnotationPair, + true) + + if (annotations.isNotEmpty()) { + return processAnnotationInjectionInner(annotations) + } + + return null + } + + private fun injectionForKotlinCall(argument: KtValueArgument, ktFunction: KtFunction, reference: PsiReference): InjectionInfo? { + val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) + val ktParameter = ktFunction.valueParameters.getOrNull(argumentIndex) ?: return null + + val patternInjection = findInjection(ktParameter, Configuration.getInstance().getInjections(KOTLIN_SUPPORT_ID)) + if (patternInjection != null) { + return patternInjection + } + + // Found psi element after resolve can be obtained from compiled declaration but annotations parameters are lost there. + // Search for original descriptor from reference. + val ktReference = reference as? KtReference ?: return null + val bindingContext = ktReference.element.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS) + val functionDescriptor = ktReference.resolveToDescriptors(bindingContext).singleOrNull() as? FunctionDescriptor ?: return null + + val parameterDescriptor = functionDescriptor.valueParameters.getOrNull(argumentIndex) ?: return null + val injectAnnotation = parameterDescriptor.annotations.findAnnotation(FqName(AnnotationUtil.LANGUAGE)) ?: return null + + val languageId = injectAnnotation.argumentValue("value")?.safeAs()?.value ?: return null + return InjectionInfo(languageId, null, null) + } + + private fun injectWithAnnotationEntry(host: KtElement): InjectionInfo? { + val argument = host.parent as? KtValueArgument ?: return null + val annotationEntry = PsiTreeUtil.getParentOfType(host, KtAnnotationEntry::class.java) ?: return null + if (isAnalyzeOff(host.project)) return null + + val calleeReference = annotationEntry.calleeExpression?.constructorReferenceExpression?.mainReference + val callee = calleeReference?.resolve() + return when (callee) { + is KtFunction -> injectionForKotlinCall(argument, callee, calleeReference) + is PsiClass -> { + // Look for java injections for the PsiAnnotationMethod. + (argument.reference ?: argument.getArgumentName()?.referenceExpression?.mainReference) + ?.let { it.resolve() as? PsiMethod } + ?.let { findInjection(it, Configuration.getInstance().getInjections(JavaLanguageInjectionSupport.JAVA_SUPPORT_ID)) } + ?.takeIf { injectionInfo -> + // Temporary forbid injection for SpEL because of inspections that gives warnings when host language is not Java. + injectionInfo.languageId != "SpEL" + } + } + else -> null + } + } + + private fun findInjection(element: PsiElement?, injections: List): InjectionInfo? { + for (injection in injections) { + if (injection.acceptsPsiElement(element)) { + return InjectionInfo(injection.injectedLanguageId, injection.prefix, injection.suffix) + } + } + + return null + } + + private fun isAnalyzeOff(project: Project): Boolean { + return Configuration.getProjectInstance(project).advancedConfiguration.dfaOption == Configuration.DfaOption.OFF + } + + private fun processAnnotationInjectionInner(annotations: Array): InjectionInfo? { + val id = AnnotationUtilEx.calcAnnotationValue(annotations, "value") + val prefix = AnnotationUtilEx.calcAnnotationValue(annotations, "prefix") + val suffix = AnnotationUtilEx.calcAnnotationValue(annotations, "suffix") + + return InjectionInfo(id, prefix, suffix) + } +} diff --git a/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt.172 b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt.172 new file mode 100644 index 00000000000..a4dc61c6497 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt.172 @@ -0,0 +1,509 @@ +/* + * 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.lang.html.HTMLLanguage +import com.intellij.openapi.fileTypes.PlainTextLanguage +import org.intellij.lang.regexp.RegExpLanguage +import org.intellij.plugins.intelliLang.Configuration +import org.intellij.plugins.intelliLang.inject.config.BaseInjection +import org.intellij.plugins.intelliLang.inject.config.InjectionPlace + +class KotlinInjectionTest : AbstractInjectionTest() { + fun testInjectionOnJavaPredefinedMethodWithAnnotation() = doInjectionPresentTest( + """ + val test1 = java.util.regex.Pattern.compile("pattern") + """, + RegExpLanguage.INSTANCE.id, + unInjectShouldBePresent = false + ) + + fun testInjectionOnJavaCustomInjectionWithAnnotation() { + val customInjection = BaseInjection("java") + customInjection.injectedLanguageId = HTMLLanguage.INSTANCE.id + val elementPattern = customInjection.compiler.createElementPattern( + """psiParameter().ofMethod(2, psiMethod().withName("replace").withParameters("int", "int", "java.lang.String").definedInClass("java.lang.StringBuilder"))""", + "HTML temp rule") + customInjection.setInjectionPlaces(InjectionPlace(elementPattern, true)) + + try { + Configuration.getInstance().replaceInjections(listOf(customInjection), listOf(), true) + + doInjectionPresentTest( + """ + val stringBuilder = StringBuilder().replace(0, 0, "") + """, + HTMLLanguage.INSTANCE.id, + unInjectShouldBePresent = false + ) + } + finally { + Configuration.getInstance().replaceInjections(listOf(), listOf(customInjection), true) + } + } + + fun testInjectionWithCommentOnProperty() = doInjectionPresentTest( + """ + //language=file-reference + val test = "simple" + """) + + fun testInjectionWithUsageOnReceiverWithRuntime() = doInjectionPresentTest( + """ + val test = "some" + fun foo() = test.toRegex() + """, + languageId = RegExpLanguage.INSTANCE.id, unInjectShouldBePresent = false) + + fun testInjectionWithUsageInParameterWithRuntime() = doInjectionPresentTest( + """ + val test = "some" + fun foo() = Regex(test) + """, + languageId = RegExpLanguage.INSTANCE.id, unInjectShouldBePresent = false) + + fun testNoInjectionThoughSeveralAssignmentsWithRuntime() = assertNoInjection( + """ + val first = "some" + val test = first + fun foo() = Regex(test) + """) + + fun testInjectionWithMultipleCommentsOnFun() = doInjectionPresentTest( + """ + // Some comment + // Other comment + //language=file-reference + fun test() = "simple" + """) + + fun testInjectionWithAnnotationOnPropertyWithAnnotation() = doInjectionPresentTest( + """ + @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" + } + """ + ) + + fun testInjectionWithUsageInFunctionWithMarkedParameterWithAnnotation() = doInjectionPresentTest( + """ + import org.intellij.lang.annotations.Language + + val v = "some" + + fun foo(@Language("HTML") s: String) {} + + fun other() { foo(v) } + """, + languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false) + + fun testInjectionOfCustomParameterWithAnnotation() = doInjectionPresentTest( + """ + import org.intellij.lang.annotations.Language + + fun foo(@Language("HTML") s: String) {} + + fun other() { foo("some") } + """, + languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false) + + fun testInjectionOfCustomParameterInConstructorWithAnnotation() = doInjectionPresentTest( + """ + import org.intellij.lang.annotations.Language + + class Test(@Language("HTML") val s: String) + + fun other() { Test("some") } + """, + languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false) + + fun testInjectionOfCustomParameterDefaultCallWithAnnotation() = doInjectionPresentTest( + """ + import org.intellij.lang.annotations.Language + + fun foo(@Language("HTML") s: String) {} + + fun other() { foo(s = "some") } + """, + languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false) + + fun testInjectionOfCustomParameterInJavaConstructorWithAnnotationWithAnnotation() = doInjectionPresentTest( + """ + fun bar() { Test("some") } + """, + javaText = + """ + import org.intellij.lang.annotations.Language; + + public class Test { + public Test(@Language("HTML") String str) {} + } + """, + languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false + ) + + fun testInjectionOfCustomParameterJavaWithAnnotation() = doInjectionPresentTest( + """ + fun bar() { Test.foo("some") } + """, + javaText = + """ + import org.intellij.lang.annotations.Language; + + public class Test { + public static void foo(@Language("HTML") String str) {} + } + """, + languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false + ) + + fun testInjectionOnInterpolationWithAnnotation() = doInjectionPresentTest( + """ + val b = 2 + + @org.intellij.lang.annotations.Language("HTML") + val test = "simple${'$'}{b}.kt" + """, + unInjectShouldBePresent = false, + shreds = listOf( + ShredInfo(range(0, 6), hostRange=range(1, 7)), + ShredInfo(range(6, 21), hostRange=range(11, 14), prefix="missingValue") + ) + ) + + fun testInjectionOnInterpolatedStringWithComment() = doInjectionPresentTest( + """ + val some = 42 + // language=HTML + val test = "ml>${'$'}some" + """, + languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false, + shreds = listOf( + ShredInfo(range(0, 6), hostRange = range(1, 7)), + ShredInfo(range(6, 17), hostRange = range(12, 19), prefix="some")) + ) + + fun testEditorShortShreadsInInterpolatedInjection() = doInjectionPresentTest( + """ + val s = 42 + // language=TEXT + val test = "${'$'}s text ${'$'}s${'$'}{s}${'$'}s text ${'$'}s" + """, + languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false, + shreds = listOf( + ShredInfo(range(0, 0), hostRange=range(1, 1)), + ShredInfo(range(0, 7), hostRange=range(3, 9), prefix="s"), + ShredInfo(range(7, 8), hostRange=range(11, 11), prefix="s"), + ShredInfo(range(8, 20), hostRange=range(15, 15), prefix="missingValue"), + ShredInfo(range(20, 27), hostRange=range(17, 23), prefix="s"), + ShredInfo(range(27, 28), hostRange=range(25, 25), prefix="s") + ) + ) + + fun testEditorLongShreadsInInterpolatedInjection() = doInjectionPresentTest( + """ + val s = 42 + // language=TEXT + val test = "${'$'}{s} text ${'$'}{s}${'$'}s${'$'}{s} text ${'$'}{s}" + """, + languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false, + shreds = listOf( + ShredInfo(range(0, 0), hostRange=range(1, 1)), + ShredInfo(range(0, 18), hostRange=range(5, 11), prefix="missingValue"), + ShredInfo(range(18, 30), hostRange=range(15, 15), prefix="missingValue"), + ShredInfo(range(30, 31), hostRange=range(17, 17), prefix="s"), + ShredInfo(range(31, 49), hostRange=range(21, 27), prefix="missingValue"), + ShredInfo(range(49, 61), hostRange=range(31, 31), prefix="missingValue") + ) + ) + + fun testEditorShreadsWithEscapingInjection() = doInjectionPresentTest( + """ + // language=TEXT + val test = "\rtext\ttext\n\t" + """, + languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false, + shreds = listOf( + ShredInfo(range(0, 12), hostRange=range(1, 17)) + ) + ) + + fun testEditorShreadsInInterpolatedWithEscapingInjection() = doInjectionPresentTest( + """ + val s = 1 + // language=TEXT + val test = "\r${'$'}s text${'$'}s\ttext\n\t" + """, + languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false, + shreds = listOf( + ShredInfo(range(0, 1), hostRange=range(1, 3)), + ShredInfo(range(1, 7), hostRange=range(5, 10), prefix="s"), + ShredInfo(range(7, 15), hostRange=range(12, 22), prefix="s") + ) + ) + + fun testSuffixPrefixWithAnnotation() = doInjectionPresentTest( + """ + @org.intellij.lang.annotations.Language("TEXT", prefix = "abc", suffix = "ghi") + val test = "def" + """, + languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false, + shreds = listOf(ShredInfo(range(0, 9), hostRange=range(1, 4), prefix = "abc", suffix = "ghi")) + ) + + fun testSuffixPrefixInComment() = doInjectionPresentTest( + """ + // language="TEXT" prefix="abc" suffix=ghi + val test = "def" + """, + languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false, + shreds = listOf(ShredInfo(range(0, 9), hostRange=range(1, 4), prefix = "abc", suffix = "ghi")) + ) + + fun testJavaAnnotationsPattern() { + val customInjection = BaseInjection("java") + customInjection.injectedLanguageId = RegExpLanguage.INSTANCE.id + val elementPattern = customInjection.compiler.createElementPattern( + """psiMethod().withName("value").withParameters().definedInClass("Matches")""", + "temp rule") + customInjection.setInjectionPlaces(InjectionPlace(elementPattern, true)) + + try { + Configuration.getInstance().replaceInjections(listOf(customInjection), listOf(), true) + + doInjectionPresentTest( + javaText = """ + @interface Matches { String value(); } + """, + text = """ + @Matches("[A-Z][a-z]+") + val name = "John" + """, + languageId = RegExpLanguage.INSTANCE.id, + unInjectShouldBePresent = false + ) + + doInjectionPresentTest( + javaText = """ + @interface Matches { String value(); } + """, + text = """ + @Matches(value = "[A-Z][a-z]+") + val name = "John" + """, + languageId = RegExpLanguage.INSTANCE.id, + unInjectShouldBePresent = false + ) + } + finally { + Configuration.getInstance().replaceInjections(listOf(), listOf(customInjection), true) + } + } + + fun testKotlinAnnotationsPattern() { + val customInjection = BaseInjection("kotlin") + customInjection.injectedLanguageId = RegExpLanguage.INSTANCE.id + val elementPattern = customInjection.compiler.createElementPattern( + """kotlinParameter().ofFunction(0, kotlinFunction().withName("Matches").definedInClass("Matches"))""".trimIndent(), + "temp rule") + customInjection.setInjectionPlaces(InjectionPlace(elementPattern, true)) + + try { + Configuration.getInstance().replaceInjections(listOf(customInjection), listOf(), true) + + doInjectionPresentTest( + text = """ + annotation class Matches(val pattern: String) + + @Matches("[A-Z][a-z]+") + val name = "John" + """, + languageId = RegExpLanguage.INSTANCE.id, + unInjectShouldBePresent = false + ) + + doInjectionPresentTest( + text = """ + annotation class Matches(val pattern: String) + + @Matches(pattern = "[A-Z][a-z]+") + val name = "John" + """, + languageId = RegExpLanguage.INSTANCE.id, + unInjectShouldBePresent = false + ) + } + finally { + Configuration.getInstance().replaceInjections(listOf(), listOf(customInjection), true) + } + } +}