initial injection support (KT-2428): allow injection inside string literals

This commit is contained in:
nik
2014-12-02 21:09:00 +03:00
parent 3ab7c6c8e9
commit ca7ce6228c
11 changed files with 444 additions and 5 deletions
@@ -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<? extends PsiLanguageInjectionHost> createLiteralTextEscaper() {
return new KotlinStringLiteralTextEscaper(this);
}
}
@@ -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<JetStringTemplateExpression>(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()
}
}
@@ -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<JetStringTemplateExpression>() {
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()
}
}
@@ -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<JetStringTemplateExpression>() {
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()
}
}
@@ -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"
= 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