Skip to content

Commit e5822c7

Browse files
committed
Allow injection in strings with interpolation (KT-6610)
#KT-6610 Fixed
1 parent 12002ae commit e5822c7

File tree

6 files changed

+263
-7
lines changed

6 files changed

+263
-7
lines changed

compiler/frontend/src/org/jetbrains/kotlin/psi/KtStringTemplateExpression.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.intellij.lang.ASTNode;
2020
import com.intellij.psi.ElementManipulators;
2121
import com.intellij.psi.LiteralTextEscaper;
22+
import com.intellij.psi.PsiElement;
2223
import com.intellij.psi.PsiLanguageInjectionHost;
2324
import com.intellij.psi.tree.TokenSet;
2425
import org.jetbrains.annotations.NotNull;
@@ -58,4 +59,14 @@ public PsiLanguageInjectionHost updateText(@NotNull String text) {
5859
public LiteralTextEscaper<? extends PsiLanguageInjectionHost> createLiteralTextEscaper() {
5960
return new KotlinStringLiteralTextEscaper(this);
6061
}
62+
63+
public boolean hasInterpolation() {
64+
for (PsiElement child : getChildren()) {
65+
if (child instanceof KtSimpleNameStringTemplateEntry || child instanceof KtBlockStringTemplateEntry) {
66+
return true;
67+
}
68+
}
69+
70+
return false;
71+
}
6172
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2010-2017 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.idea.injection
18+
19+
import com.intellij.openapi.util.TextRange
20+
import com.intellij.openapi.util.Trinity
21+
import com.intellij.psi.PsiElement
22+
import com.intellij.psi.PsiLanguageInjectionHost
23+
import org.intellij.plugins.intelliLang.inject.InjectedLanguage
24+
import org.intellij.plugins.intelliLang.inject.InjectorUtils
25+
import org.intellij.plugins.intelliLang.inject.config.BaseInjection
26+
import org.jetbrains.kotlin.psi.*
27+
import java.util.*
28+
29+
data class InjectionSplitResult(val isUnparsable: Boolean, val ranges: List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>>)
30+
31+
fun splitLiteralToInjectionParts(injection: BaseInjection, literal: KtStringTemplateExpression): InjectionSplitResult? {
32+
InjectorUtils.getLanguage(injection) ?: return null
33+
34+
val children = literal.children.toList()
35+
val len = children.size
36+
37+
if (children.isEmpty()) return null
38+
39+
val result = ArrayList<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>>()
40+
41+
fun addInjectionRange(range: TextRange, prefix: String, suffix: String) {
42+
TextRange.assertProperRange(range, injection)
43+
val injectedLanguage = InjectedLanguage.create(injection.injectedLanguageId, prefix, suffix, true)!!
44+
result.add(Trinity.create(literal, injectedLanguage, range))
45+
}
46+
47+
var unparsable = false
48+
49+
var prefix = injection.prefix
50+
val lastChild = children.lastOrNull()
51+
52+
var i = 0
53+
while (i < len) {
54+
val child = children[i]
55+
val partOffsetInParent = child.startOffsetInParent
56+
57+
val part = when (child) {
58+
is KtLiteralStringTemplateEntry, is KtEscapeStringTemplateEntry -> {
59+
val partSize = children.subList(i, len).asSequence()
60+
.takeWhile { it is KtLiteralStringTemplateEntry || it is KtEscapeStringTemplateEntry }
61+
.count()
62+
i += partSize - 1
63+
children[i]
64+
}
65+
is KtSimpleNameStringTemplateEntry -> {
66+
unparsable = true
67+
child.expression?.text ?: NO_VALUE_NAME
68+
}
69+
is KtBlockStringTemplateEntry -> {
70+
unparsable = true
71+
NO_VALUE_NAME
72+
}
73+
else -> {
74+
unparsable = true
75+
child
76+
}
77+
}
78+
79+
val suffix = if (child == lastChild) injection.suffix else ""
80+
81+
if (part is PsiElement) {
82+
addInjectionRange(TextRange.create(partOffsetInParent, part.startOffsetInParent + part.textLength), prefix, suffix)
83+
}
84+
else if (!prefix.isEmpty() || i == 0) {
85+
addInjectionRange(TextRange.from(partOffsetInParent, 0), prefix, suffix)
86+
}
87+
88+
prefix = part as? String ?: ""
89+
i++
90+
}
91+
92+
if (lastChild != null && !prefix.isEmpty()) {
93+
// Last element was interpolated part, need to add a range after it
94+
addInjectionRange(TextRange.from(lastChild.startOffsetInParent + lastChild.textLength, 0), prefix, injection.suffix)
95+
}
96+
97+
return InjectionSplitResult(unparsable, result)
98+
}
99+
100+
private val NO_VALUE_NAME = "missingValue"

idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ class KotlinLanguageInjectionSupport : AbstractLanguageInjectionSupport() {
5757
val configuration = Configuration.getProjectInstance(host.project).advancedConfiguration
5858
if (!configuration.isSourceModificationAllowed) {
5959
// It's not allowed to modify code without explicit permission. Postpone adding @Inject or comment till it granted.
60-
host.putUserData(InjectLanguageAction.FIX_KEY, Processor { host -> addInjectionInstructionInCode(language, host) })
60+
host.putUserData(InjectLanguageAction.FIX_KEY, Processor { fixHost ->
61+
addInjectionInstructionInCode(language, fixHost)
62+
})
6163
return false
6264
}
6365

idea/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjector.kt

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ import com.intellij.psi.PsiAnnotation
2525
import com.intellij.psi.PsiElement
2626
import com.intellij.psi.PsiMethod
2727
import com.intellij.psi.PsiReference
28+
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
2829
import com.intellij.psi.search.LocalSearchScope
2930
import com.intellij.psi.search.searches.ReferencesSearch
3031
import com.intellij.psi.util.PsiTreeUtil
3132
import org.intellij.plugins.intelliLang.Configuration
33+
import org.intellij.plugins.intelliLang.inject.InjectedLanguage
3234
import org.intellij.plugins.intelliLang.inject.InjectorUtils
35+
import org.intellij.plugins.intelliLang.inject.LanguageInjectionSupport
36+
import org.intellij.plugins.intelliLang.inject.TemporaryPlacesRegistry
3337
import org.intellij.plugins.intelliLang.inject.config.BaseInjection
3438
import org.intellij.plugins.intelliLang.inject.java.JavaLanguageInjectionSupport
3539
import org.intellij.plugins.intelliLang.util.AnnotationUtilEx
@@ -44,7 +48,10 @@ import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
4448
import java.util.*
4549
import kotlin.collections.ArrayList
4650

47-
class KotlinLanguageInjector : MultiHostInjector {
51+
class KotlinLanguageInjector(
52+
val configuration: Configuration,
53+
val project: Project,
54+
val temporaryPlacesRegistry: TemporaryPlacesRegistry) : MultiHostInjector {
4855
companion object {
4956
private val STRING_LITERALS_REGEXP = "\"([^\"]*)\"".toRegex()
5057
}
@@ -61,10 +68,38 @@ class KotlinLanguageInjector : MultiHostInjector {
6168

6269
if (!ProjectRootsUtil.isInProjectOrLibSource(ktHost)) return
6370

64-
val injectionInfo = findInjectionInfo(context) ?: return
65-
InjectorUtils.getLanguageByString(injectionInfo.languageId) ?: return
71+
val containingFile = ktHost.containingFile
72+
val tempInjectedLanguage: InjectedLanguage? = temporaryPlacesRegistry.getLanguageFor(ktHost, containingFile)
6673

67-
InjectorUtils.registerInjectionSimple(ktHost, injectionInfo.toBaseInjection(support)!!, support, registrar)
74+
val baseInjection: BaseInjection = if (tempInjectedLanguage == null) {
75+
val injectionInfo = findInjectionInfo(context) ?: return
76+
injectionInfo.toBaseInjection(support)
77+
}
78+
else {
79+
InjectorUtils.putInjectedFileUserData(registrar, LanguageInjectionSupport.TEMPORARY_INJECTED_LANGUAGE, tempInjectedLanguage)
80+
BaseInjection(support.id).apply {
81+
injectedLanguageId = tempInjectedLanguage.id
82+
prefix = tempInjectedLanguage.prefix
83+
suffix = tempInjectedLanguage.suffix
84+
}
85+
} ?: return
86+
87+
val language = InjectorUtils.getLanguageByString(baseInjection.injectedLanguageId) ?: return
88+
89+
if (ktHost.hasInterpolation()) {
90+
val file = ktHost.containingKtFile
91+
val parts = splitLiteralToInjectionParts(baseInjection, ktHost) ?: return
92+
93+
if (parts.ranges.isEmpty()) return
94+
95+
InjectorUtils.registerInjection(language, parts.ranges, file, registrar)
96+
InjectorUtils.registerSupport(support, false, registrar)
97+
InjectorUtils.putInjectedFileUserData(registrar, InjectedLanguageUtil.FRANKENSTEIN_INJECTION,
98+
if (parts.isUnparsable) java.lang.Boolean.TRUE else null)
99+
}
100+
else {
101+
InjectorUtils.registerInjectionSimple(ktHost, baseInjection, support, registrar)
102+
}
68103
}
69104

70105
override fun elementsToInjectIn(): List<Class<out PsiElement>> {

idea/tests/org/jetbrains/kotlin/psi/AbstractInjectionTest.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.jetbrains.kotlin.psi
1818

19+
import com.intellij.injected.editor.DocumentWindowImpl
1920
import com.intellij.injected.editor.EditorWindow
21+
import com.intellij.openapi.util.TextRange
2022
import com.intellij.psi.injection.Injectable
2123
import com.intellij.testFramework.LightProjectDescriptor
2224
import junit.framework.TestCase
@@ -40,16 +42,34 @@ abstract class AbstractInjectionTest : KotlinLightCodeInsightFixtureTestCase() {
4042
}
4143
}
4244

45+
data class ShredInfo(
46+
val range: TextRange,
47+
val hostRange: TextRange,
48+
val prefix: String = "",
49+
val suffix: String = "") {
50+
}
51+
4352
protected fun doInjectionPresentTest(
4453
@Language("kotlin") text: String, @Language("Java") javaText: String? = null,
45-
languageId: String? = null, unInjectShouldBePresent: Boolean = true) {
54+
languageId: String? = null, unInjectShouldBePresent: Boolean = true,
55+
shreds: List<ShredInfo>? = null) {
4656
if (javaText != null) {
4757
myFixture.configureByText("${getTestName(true)}.java", javaText.trimIndent())
4858
}
4959

5060
myFixture.configureByText("${getTestName(true)}.kt", text.trimIndent())
5161

5262
assertInjectionPresent(languageId, unInjectShouldBePresent)
63+
64+
if (shreds != null) {
65+
val actualShreds = (editor.document as DocumentWindowImpl).shreds.map {
66+
ShredInfo(it.range, it.rangeInsideHost, it.prefix, it.suffix)
67+
}
68+
69+
assertOrderedEquals(
70+
actualShreds.sortedBy { it.range.startOffset },
71+
shreds.sortedBy { it.range.startOffset })
72+
}
5373
}
5474

5575
protected fun assertInjectionPresent(languageId: String?, unInjectShouldBePresent: Boolean) {
@@ -103,4 +123,6 @@ abstract class AbstractInjectionTest : KotlinLightCodeInsightFixtureTestCase() {
103123
configuration.isSourceModificationAllowed = allowed
104124
}
105125
}
106-
}
126+
127+
fun range(start: Int, end: Int) = TextRange.create(start, end)
128+
}

idea/tests/org/jetbrains/kotlin/psi/KotlinInjectionTest.kt

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.jetbrains.kotlin.psi
1818

1919
import com.intellij.lang.html.HTMLLanguage
20+
import com.intellij.openapi.fileTypes.PlainTextLanguage
2021
import org.intellij.lang.regexp.RegExpLanguage
2122
import org.intellij.plugins.intelliLang.Configuration
2223
import org.intellij.plugins.intelliLang.inject.config.BaseInjection
@@ -324,4 +325,89 @@ class KotlinInjectionTest : AbstractInjectionTest() {
324325
""",
325326
languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false
326327
)
328+
329+
fun testInjectionOnInterpolationWithAnnotation() = doInjectionPresentTest(
330+
"""
331+
val b = 2
332+
333+
@org.intellij.lang.annotations.Language("HTML")
334+
val test = "<caret>simple${'$'}{b}.kt"
335+
""",
336+
unInjectShouldBePresent = false,
337+
shreds = listOf(
338+
ShredInfo(range(0, 6), hostRange=range(1, 7)),
339+
ShredInfo(range(6, 21), hostRange=range(11, 14), prefix="missingValue")
340+
)
341+
)
342+
343+
fun testInjectionOnInterpolatedStringWithComment() = doInjectionPresentTest(
344+
"""
345+
val some = 42
346+
// language=HTML
347+
val test = "<ht<caret>ml>${'$'}some</html>"
348+
""",
349+
languageId = HTMLLanguage.INSTANCE.id, unInjectShouldBePresent = false,
350+
shreds = listOf(
351+
ShredInfo(range(0, 6), hostRange = range(1, 7)),
352+
ShredInfo(range(6, 17), hostRange = range(12, 19), prefix="some"))
353+
)
354+
355+
fun testEditorShortShreadsInInterpolatedInjection() = doInjectionPresentTest(
356+
"""
357+
val s = 42
358+
// language=TEXT
359+
val test = "${'$'}s <caret>text ${'$'}s${'$'}{s}${'$'}s text ${'$'}s"
360+
""",
361+
languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false,
362+
shreds = listOf(
363+
ShredInfo(range(0, 0), hostRange=range(1, 1)),
364+
ShredInfo(range(0, 7), hostRange=range(3, 9), prefix="s"),
365+
ShredInfo(range(7, 8), hostRange=range(11, 11), prefix="s"),
366+
ShredInfo(range(8, 20), hostRange=range(15, 15), prefix="missingValue"),
367+
ShredInfo(range(20, 27), hostRange=range(17, 23), prefix="s"),
368+
ShredInfo(range(27, 28), hostRange=range(25, 25), prefix="s")
369+
)
370+
)
371+
372+
fun testEditorLongShreadsInInterpolatedInjection() = doInjectionPresentTest(
373+
"""
374+
val s = 42
375+
// language=TEXT
376+
val test = "${'$'}{s} <caret>text ${'$'}{s}${'$'}s${'$'}{s} text ${'$'}{s}"
377+
""",
378+
languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false,
379+
shreds = listOf(
380+
ShredInfo(range(0, 0), hostRange=range(1, 1)),
381+
ShredInfo(range(0, 18), hostRange=range(5, 11), prefix="missingValue"),
382+
ShredInfo(range(18, 30), hostRange=range(15, 15), prefix="missingValue"),
383+
ShredInfo(range(30, 31), hostRange=range(17, 17), prefix="s"),
384+
ShredInfo(range(31, 49), hostRange=range(21, 27), prefix="missingValue"),
385+
ShredInfo(range(49, 61), hostRange=range(31, 31), prefix="missingValue")
386+
)
387+
)
388+
389+
fun testEditorShreadsWithEscapingInjection() = doInjectionPresentTest(
390+
"""
391+
// language=TEXT
392+
val test = "\rte<caret>xt\ttext\n\t"
393+
""",
394+
languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false,
395+
shreds = listOf(
396+
ShredInfo(range(0, 12), hostRange=range(1, 17))
397+
)
398+
)
399+
400+
fun testEditorShreadsInInterpolatedWithEscapingInjection() = doInjectionPresentTest(
401+
"""
402+
val s = 1
403+
// language=TEXT
404+
val test = "\r${'$'}s te<caret>xt${'$'}s\ttext\n\t"
405+
""",
406+
languageId = PlainTextLanguage.INSTANCE.id, unInjectShouldBePresent = false,
407+
shreds = listOf(
408+
ShredInfo(range(0, 1), hostRange=range(1, 3)),
409+
ShredInfo(range(1, 7), hostRange=range(5, 10), prefix="s"),
410+
ShredInfo(range(7, 15), hostRange=range(12, 22), prefix="s")
411+
)
412+
)
327413
}

0 commit comments

Comments
 (0)