From ca7ce6228c4a0987fdfd4dee41895f5e18189aa5 Mon Sep 17 00:00:00 2001 From: nik Date: Tue, 2 Dec 2014 21:09:00 +0300 Subject: [PATCH] initial injection support (KT-2428): allow injection inside string literals --- .idea/libraries/intellilang_plugin.xml | 12 ++ .../lang/psi/JetStringTemplateExpression.java | 34 +++- .../psi/KotlinStringLiteralTextEscaper.kt | 81 ++++++++++ .../JetStringTemplateExpressionManipulator.kt | 39 +++++ .../StringTemplateExpressionManipulator.kt | 39 +++++ .../jet/lang/psi/psiUtil/jetPsiUtil.kt | 13 +- idea/idea.iml | 4 +- idea/src/META-INF/plugin.xml | 2 + .../jet/plugin/DirectiveBasedActionUtils.java | 2 +- ...StringTemplateExpressionManipulatorTest.kt | 73 +++++++++ .../psi/injection/StringInjectionHostTest.kt | 150 ++++++++++++++++++ 11 files changed, 444 insertions(+), 5 deletions(-) create mode 100644 .idea/libraries/intellilang_plugin.xml create mode 100644 compiler/frontend/src/org/jetbrains/jet/lang/psi/KotlinStringLiteralTextEscaper.kt create mode 100644 compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/JetStringTemplateExpressionManipulator.kt create mode 100644 compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/StringTemplateExpressionManipulator.kt create mode 100644 idea/tests/org/jetbrains/jet/psi/StringTemplateExpressionManipulatorTest.kt create mode 100644 idea/tests/org/jetbrains/jet/psi/injection/StringInjectionHostTest.kt diff --git a/.idea/libraries/intellilang_plugin.xml b/.idea/libraries/intellilang_plugin.xml new file mode 100644 index 00000000000..1a00de34e63 --- /dev/null +++ b/.idea/libraries/intellilang_plugin.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/jet/lang/psi/JetStringTemplateExpression.java b/compiler/frontend/src/org/jetbrains/jet/lang/psi/JetStringTemplateExpression.java index 6ccfb13c7d9..882827f5818 100644 --- a/compiler/frontend/src/org/jetbrains/jet/lang/psi/JetStringTemplateExpression.java +++ b/compiler/frontend/src/org/jetbrains/jet/lang/psi/JetStringTemplateExpression.java @@ -17,9 +17,18 @@ package org.jetbrains.jet.lang.psi; import com.intellij.lang.ASTNode; +import com.intellij.psi.ElementManipulators; +import com.intellij.psi.LiteralTextEscaper; +import com.intellij.psi.PsiLanguageInjectionHost; +import com.intellij.psi.tree.TokenSet; import org.jetbrains.annotations.NotNull; +import org.jetbrains.jet.JetNodeTypes; +import org.jetbrains.jet.lexer.JetTokens; + + +public class JetStringTemplateExpression extends JetExpressionImpl implements PsiLanguageInjectionHost { + private static final TokenSet TOKENS_SUITABLE_FOR_INJECTION = TokenSet.create(JetNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY, JetNodeTypes.ESCAPE_STRING_TEMPLATE_ENTRY); -public class JetStringTemplateExpression extends JetExpressionImpl { public JetStringTemplateExpression(@NotNull ASTNode node) { super(node); } @@ -33,4 +42,27 @@ public class JetStringTemplateExpression extends JetExpressionImpl { public JetStringTemplateEntry[] getEntries() { return findChildrenByClass(JetStringTemplateEntry.class); } + + @Override + public boolean isValidHost() { + ASTNode node = getNode(); + ASTNode child = node.getFirstChildNode().getTreeNext(); + while (child != null) { + if (child.getElementType() == JetTokens.CLOSING_QUOTE) return true; + if (!TOKENS_SUITABLE_FOR_INJECTION.contains(child.getElementType())) return false; + child = child.getTreeNext(); + } + return false; + } + + @Override + public PsiLanguageInjectionHost updateText(@NotNull String text) { + return ElementManipulators.handleContentChange(this, text); + } + + @NotNull + @Override + public LiteralTextEscaper createLiteralTextEscaper() { + return new KotlinStringLiteralTextEscaper(this); + } } diff --git a/compiler/frontend/src/org/jetbrains/jet/lang/psi/KotlinStringLiteralTextEscaper.kt b/compiler/frontend/src/org/jetbrains/jet/lang/psi/KotlinStringLiteralTextEscaper.kt new file mode 100644 index 00000000000..a5c9b31ad11 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/jet/lang/psi/KotlinStringLiteralTextEscaper.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2014 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.jet.lang.psi + +import com.intellij.psi.LiteralTextEscaper +import com.intellij.openapi.util.TextRange +import gnu.trove.TIntArrayList +import org.jetbrains.jet.lang.psi.psiUtil.getContentRange +import org.jetbrains.jet.lang.psi.psiUtil.isSingleQuoted + +public class KotlinStringLiteralTextEscaper(host: JetStringTemplateExpression): LiteralTextEscaper(host) { + private var sourceOffsets: IntArray? = null + + override fun decode(rangeInsideHost: TextRange, outChars: StringBuilder): Boolean { + val sourceOffsetsList = TIntArrayList() + var sourceOffset = 0 + + for (child in myHost.getEntries()) { + val childRange = TextRange.from(child.getStartOffsetInParent(), child.getTextLength()) + if (rangeInsideHost.getEndOffset() <= childRange.getStartOffset()) { + break + } + if (childRange.getEndOffset() <= rangeInsideHost.getStartOffset()) { + continue + } + when (child) { + is JetLiteralStringTemplateEntry -> { + val textRange = rangeInsideHost.intersection(childRange).shiftRight(-childRange.getStartOffset()) + outChars.append(child.getText(), textRange.getStartOffset(), textRange.getEndOffset()) + textRange.getLength().times { + sourceOffsetsList.add(sourceOffset++) + } + } + is JetEscapeStringTemplateEntry -> { + if (!rangeInsideHost.contains(childRange)) { + //don't allow injection if its range starts or ends inside escaped sequence + return false + } + val unescaped = child.getUnescapedValue() + outChars.append(unescaped) + unescaped.length().times { + sourceOffsetsList.add(sourceOffset) + } + sourceOffset += child.getTextLength() + } + else -> return false + } + } + sourceOffsetsList.add(sourceOffset) + sourceOffsets = sourceOffsetsList.toNativeArray() + return true + } + + override fun getOffsetInHost(offsetInDecoded: Int, rangeInsideHost: TextRange): Int { + val offsets = sourceOffsets + if (offsets == null || offsetInDecoded >= offsets.size()) return -1 + return Math.min(offsets[offsetInDecoded], rangeInsideHost.getLength()) + rangeInsideHost.getStartOffset() + } + + override fun getRelevantTextRange(): TextRange { + return myHost.getContentRange() + } + + override fun isOneLine(): Boolean { + return myHost.isSingleQuoted() + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/JetStringTemplateExpressionManipulator.kt b/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/JetStringTemplateExpressionManipulator.kt new file mode 100644 index 00000000000..6d81d9d2d91 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/JetStringTemplateExpressionManipulator.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2014 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.jet.lang.psi.psiUtil + +import com.intellij.psi.AbstractElementManipulator +import org.jetbrains.jet.lang.psi.JetStringTemplateExpression +import com.intellij.openapi.util.TextRange +import org.jetbrains.jet.lang.psi.JetPsiFactory +import com.intellij.openapi.util.text.StringUtil + +public class JetStringTemplateExpressionManipulator : AbstractElementManipulator() { + override fun handleContentChange(element: JetStringTemplateExpression, range: TextRange, newContent: String): JetStringTemplateExpression? { + val node = element.getNode() + val content = if (element.isSingleQuoted()) StringUtil.escapeStringCharacters(newContent) else newContent + val oldText = node.getText() + val newText = oldText.substring(0, range.getStartOffset()) + content + oldText.substring(range.getEndOffset()) + val expression = JetPsiFactory(element.getProject()).createExpression(newText) + node.replaceAllChildrenToChildrenOf(expression.getNode()) + return node.getPsi(javaClass()) + } + + override fun getRangeInElement(element: JetStringTemplateExpression): TextRange { + return element.getContentRange() + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/StringTemplateExpressionManipulator.kt b/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/StringTemplateExpressionManipulator.kt new file mode 100644 index 00000000000..7c56aa4e038 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/StringTemplateExpressionManipulator.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2014 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.jet.lang.psi.psiUtil + +import com.intellij.psi.AbstractElementManipulator +import org.jetbrains.jet.lang.psi.JetStringTemplateExpression +import com.intellij.openapi.util.TextRange +import org.jetbrains.jet.lang.psi.JetPsiFactory +import com.intellij.openapi.util.text.StringUtil + +public class StringTemplateExpressionManipulator: AbstractElementManipulator() { + override fun handleContentChange(element: JetStringTemplateExpression, range: TextRange, newContent: String): JetStringTemplateExpression? { + val node = element.getNode() + val content = if (node.getFirstChildNode().getTextLength() == 1) StringUtil.escapeStringCharacters(newContent) else newContent + val oldText = node.getText() + val newText = oldText.substring(0, range.getStartOffset()) + content + oldText.substring(range.getEndOffset()) + val expression = JetPsiFactory(element.getProject()).createExpression(newText) + node.replaceAllChildrenToChildrenOf(expression.getNode()) + return node.getPsi(javaClass()) + } + + override fun getRangeInElement(element: JetStringTemplateExpression): TextRange { + return element.getContentRange() + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/jetPsiUtil.kt b/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/jetPsiUtil.kt index 93b35105160..378ff386c97 100644 --- a/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/jetPsiUtil.kt +++ b/compiler/frontend/src/org/jetbrains/jet/lang/psi/psiUtil/jetPsiUtil.kt @@ -38,6 +38,7 @@ import org.jetbrains.jet.lang.diagnostics.DiagnosticUtils import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.PsiComment import org.jetbrains.jet.lang.resolve.calls.CallTransformer.CallForImplicitInvoke +import com.intellij.openapi.util.TextRange public fun JetCallElement.getCallNameExpression(): JetSimpleNameExpression? { val calleeExpression = getCalleeExpression() @@ -409,4 +410,14 @@ public fun JetTypeReference?.isProbablyNothing(): Boolean { } public fun JetUserType?.isProbablyNothing(): Boolean - = this?.getReferencedName() == "Nothing" \ No newline at end of file + = this?.getReferencedName() == "Nothing" + +public fun JetStringTemplateExpression.getContentRange(): TextRange { + val start = getNode().getFirstChildNode().getTextLength() + val lastChild = getNode().getLastChildNode() + val length = getTextLength() + return TextRange(start, if (lastChild.getElementType() == JetTokens.CLOSING_QUOTE) length - lastChild.getTextLength() else length) +} + +public fun JetStringTemplateExpression.isSingleQuoted(): Boolean + = getNode().getFirstChildNode().getTextLength() == 1 diff --git a/idea/idea.iml b/idea/idea.iml index 607544ed1b5..21412d50803 100644 --- a/idea/idea.iml +++ b/idea/idea.iml @@ -24,6 +24,7 @@ + @@ -44,5 +45,4 @@ - - + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 388bc265dbb..2f695c90203 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -221,6 +221,8 @@ + diff --git a/idea/tests/org/jetbrains/jet/plugin/DirectiveBasedActionUtils.java b/idea/tests/org/jetbrains/jet/plugin/DirectiveBasedActionUtils.java index 71c3a865df4..33c5aa41c6e 100644 --- a/idea/tests/org/jetbrains/jet/plugin/DirectiveBasedActionUtils.java +++ b/idea/tests/org/jetbrains/jet/plugin/DirectiveBasedActionUtils.java @@ -105,5 +105,5 @@ public class DirectiveBasedActionUtils { } private static final Collection IRRELEVANT_ACTION_PREFIXES = - Arrays.asList("Disable ", "Edit intention settings", "Edit inspection profile setting"); + Arrays.asList("Disable ", "Edit intention settings", "Edit inspection profile setting", "Inject Language/Reference"); } diff --git a/idea/tests/org/jetbrains/jet/psi/StringTemplateExpressionManipulatorTest.kt b/idea/tests/org/jetbrains/jet/psi/StringTemplateExpressionManipulatorTest.kt new file mode 100644 index 00000000000..9f86abc1598 --- /dev/null +++ b/idea/tests/org/jetbrains/jet/psi/StringTemplateExpressionManipulatorTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2014 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.jet.psi + +import org.jetbrains.jet.plugin.JetLightCodeInsightFixtureTestCase +import org.jetbrains.jet.plugin.JetLightProjectDescriptor +import org.jetbrains.jet.lang.psi.JetStringTemplateExpression +import org.jetbrains.jet.lang.psi.JetPsiFactory +import com.intellij.psi.ElementManipulators +import org.junit.Assert.* +import com.intellij.openapi.util.TextRange + +public class StringTemplateExpressionManipulatorTest: JetLightCodeInsightFixtureTestCase() { + public fun testSingleQuoted() { + doTestContentChange("\"a\"", "b", "\"b\"") + doTestContentChange("\"\"", "b", "\"b\"") + doTestContentChange("\"a\"", "\t", "\"\\t\"") + doTestContentChange("\"a\"", "\n", "\"\\n\"") + doTestContentChange("\"a\"", "\\t", "\"\\\\t\"") + } + + public fun testUnclosedQuoted() { + doTestContentChange("\"a", "b", "\"b") + doTestContentChange("\"", "b", "\"b") + doTestContentChange("\"a", "\t", "\"\\t") + doTestContentChange("\"a", "\n", "\"\\n") + doTestContentChange("\"a", "\\t", "\"\\\\t") + } + + public fun testTripleQuoted() { + doTestContentChange("\"\"\"a\"\"\"", "b", "\"\"\"b\"\"\"") + doTestContentChange("\"\"\"\"\"\"", "b", "\"\"\"b\"\"\"") + doTestContentChange("\"\"\"a\"\"\"", "\t", "\"\"\"\t\"\"\"") + doTestContentChange("\"\"\"a\"\"\"", "\n", "\"\"\"\n\"\"\"") + doTestContentChange("\"\"\"a\"\"\"", "\\t", "\"\"\"\\t\"\"\"") + } + + public fun testUnclosedTripleQuoted() { + doTestContentChange("\"\"\"a", "b", "\"\"\"b") + doTestContentChange("\"\"\"", "b", "\"\"\"b") + doTestContentChange("\"\"\"a", "\t", "\"\"\"\t") + doTestContentChange("\"\"\"a", "\n", "\"\"\"\n") + doTestContentChange("\"\"\"a", "\\t", "\"\"\"\\t") + } + + public fun testReplaceRange() { + doTestContentChange("\"abc\"", "x", range = TextRange(2,3), expected = "\"axc\"") + doTestContentChange("\"\"\"abc\"\"\"", "x", range = TextRange(4,5), expected = "\"\"\"axc\"\"\"") + } + + private fun doTestContentChange(original: String, newContent: String, expected: String, range: TextRange? = null) { + val expression = JetPsiFactory(getProject()).createExpression(original) as JetStringTemplateExpression + val manipulator = ElementManipulators.getNotNullManipulator(expression) + val newExpression = if (range == null) manipulator.handleContentChange(expression, newContent) else manipulator.handleContentChange(expression, range, newContent) + assertEquals(expected, newExpression.getText()) + } + + override fun getProjectDescriptor() = JetLightProjectDescriptor.INSTANCE +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/jet/psi/injection/StringInjectionHostTest.kt b/idea/tests/org/jetbrains/jet/psi/injection/StringInjectionHostTest.kt new file mode 100644 index 00000000000..785acba3b76 --- /dev/null +++ b/idea/tests/org/jetbrains/jet/psi/injection/StringInjectionHostTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2010-2014 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.jet.psi.injection + +import org.jetbrains.jet.lang.psi.JetPsiFactory +import org.jetbrains.jet.lang.psi.JetStringTemplateExpression +import org.junit.Assert.* +import com.intellij.openapi.util.TextRange +import java.util.HashMap +import org.jetbrains.jet.JetLiteFixture +import org.jetbrains.jet.ConfigurationKind + +public class StringInjectionHostTest: JetLiteFixture() { + public fun testRegular() { + with (quoted("")) { + checkInjection("", mapOf(0 to 1)) + assertOneLine() + } + with (quoted("a")) { + checkInjection("a", mapOf(0 to 1, 1 to 2)) + assertOneLine() + } + with (quoted("ab")) { + checkInjection("ab", mapOf(0 to 1, 1 to 2, 2 to 3)) + checkInjection("a", mapOf(0 to 1, 1 to 2), rangeInHost = TextRange(1, 2)) + checkInjection("b", mapOf(0 to 2, 1 to 3), rangeInHost = TextRange(2, 3)) + assertOneLine() + } + } + + public fun testUnclosedSimpleLiteral() { + assertFalse(stringExpression("\"").isValidHost()); + assertFalse(stringExpression("\"a").isValidHost()); + } + + public fun testEscapeSequences() { + with (quoted("\\t")) { + checkInjection("\t", mapOf(0 to 1, 1 to 3)) + assertNoInjection(TextRange(1, 2)) + assertNoInjection(TextRange(2, 3)) + assertOneLine() + } + + with (quoted("a\\tb")) { + checkInjection("a\tb", mapOf(0 to 1, 1 to 2, 2 to 4, 3 to 5)) + checkInjection("a", mapOf(0 to 1, 1 to 2), rangeInHost = TextRange(1, 2)) + assertNoInjection(TextRange(1, 3)) + checkInjection("a\t", mapOf(0 to 1, 1 to 2, 2 to 4), rangeInHost = TextRange(1, 4)) + checkInjection("\t", mapOf(0 to 2, 1 to 4), rangeInHost = TextRange(2, 4)) + checkInjection("\tb", mapOf(0 to 2, 1 to 4, 2 to 5), rangeInHost = TextRange(2, 5)) + assertOneLine() + } + } + + public fun testTripleQuotes() { + with (tripleQuoted("")) { + checkInjection("", mapOf(0 to 3)) + assertMultiLine() + } + with (tripleQuoted("a")) { + checkInjection("a", mapOf(0 to 3, 1 to 4)) + assertMultiLine() + } + with (tripleQuoted("ab")) { + checkInjection("ab", mapOf(0 to 3, 1 to 4, 2 to 5)) + checkInjection("a", mapOf(0 to 3, 1 to 4), rangeInHost = TextRange(3, 4)) + checkInjection("b", mapOf(0 to 4, 1 to 5), rangeInHost = TextRange(4, 5)) + assertMultiLine() + } + } + + public fun testEscapeSequenceInTripleQuotes() { + with (tripleQuoted("\\t")) { + checkInjection("\\t", mapOf(0 to 3, 1 to 4, 2 to 5)) + checkInjection("\\", mapOf(0 to 3, 1 to 4), rangeInHost = TextRange(3, 4)) + checkInjection("t", mapOf(0 to 4, 1 to 5), rangeInHost = TextRange(4, 5)) + assertMultiLine() + } + } + + public fun testMultiLine() { + with (tripleQuoted("a\nb")) { + checkInjection("a\nb", mapOf(0 to 3, 1 to 4, 2 to 5, 3 to 6)) + assertMultiLine() + } + } + + private fun quoted(s: String): JetStringTemplateExpression { + return stringExpression("\"$s\"") + } + + private fun tripleQuoted(s: String): JetStringTemplateExpression { + return stringExpression("\"\"\"$s\"\"\"") + } + + private fun stringExpression(s: String): JetStringTemplateExpression { + return JetPsiFactory(getProject()).createExpression(s) as JetStringTemplateExpression + } + + private fun JetStringTemplateExpression.assertNoInjection(range: TextRange): JetStringTemplateExpression { + assertTrue(isValidHost()) + assertFalse(createLiteralTextEscaper().decode(range, StringBuilder())) + return this + } + + private fun JetStringTemplateExpression.assertOneLine() { + assertTrue(createLiteralTextEscaper().isOneLine()) + } + + private fun JetStringTemplateExpression.assertMultiLine() { + assertFalse(createLiteralTextEscaper().isOneLine()) + } + + //todo[nik] make private when KT-6382 is fixed + fun JetStringTemplateExpression.checkInjection(decoded: String, targetToSourceOffsets: Map, rangeInHost: TextRange? = null) { + assertTrue(isValidHost()) + for (prefix in listOf("", "prefix")) { + val escaper = createLiteralTextEscaper() + val chars = StringBuilder(prefix) + val range = rangeInHost ?: escaper.getRelevantTextRange() + assertTrue(escaper.decode(range, chars)) + assertEquals(decoded, chars.substring(prefix.length())) + val extendedOffsets = HashMap(targetToSourceOffsets) + val beforeStart = targetToSourceOffsets.keySet().min()!! - 1 + if (beforeStart >= 0) { + extendedOffsets[beforeStart] = -1 + } + extendedOffsets[targetToSourceOffsets.keySet().max()!! + 1] = -1 + for ((target, source) in extendedOffsets) { + assertEquals("Wrong source offset for $target", source, escaper.getOffsetInHost(target, range)) + } + } + } + + override fun createEnvironment() = createEnvironmentWithMockJdk(ConfigurationKind.JDK_ONLY) +}