Skip to content

Commit

Permalink
Use NixStringUtil in NixStringLiteralEscaper
Browse files Browse the repository at this point in the history
  • Loading branch information
JojOatXGME committed Jul 15, 2024
1 parent f47a403 commit 328d644
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 163 deletions.
166 changes: 56 additions & 110 deletions src/main/java/org/nixos/idea/psi/NixStringLiteralEscaper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,139 +2,85 @@ package org.nixos.idea.psi

import com.intellij.openapi.util.TextRange
import com.intellij.psi.LiteralTextEscaper
import com.intellij.psi.PsiLanguageInjectionHost
import org.intellij.lang.annotations.Language
import org.nixos.idea.psi.impl.AbstractNixString
import org.nixos.idea.util.NixStringUtil

class NixStringLiteralEscaper(host: AbstractNixString) : LiteralTextEscaper<PsiLanguageInjectionHost>(host) {
class NixStringLiteralEscaper(host: AbstractNixString) : LiteralTextEscaper<NixString>(host) {

override fun isOneLine(): Boolean = false

private var outSourceOffsets: IntArray? = null

override fun getRelevantTextRange(): TextRange {
if (myHost.textLength <= 4) return TextRange.EMPTY_RANGE
return TextRange.create(2, myHost.textLength - 2)
val parts = myHost.stringParts
return if (parts.isEmpty()) TextRange.EMPTY_RANGE
else TextRange.create(parts.first().startOffsetInParent, parts.last().textRangeInParent.endOffset)
}

override fun decode(rangeInsideHost: TextRange, outChars: StringBuilder): Boolean {
// TODO issue #81 only indented strings supported for now
// single line strings require a new decode function because
// it uses different escaping mechanisms
if (myHost !is NixIndString) return false

val maxIndent = NixStringUtil.detectMaxIndent(myHost)
val subText: String = rangeInsideHost.substring(myHost.text)
val outOffset = outChars.length
val array = IntArray(subText.length + 1)
val success = unescapeAndDecode(subText, outChars, array)
outSourceOffsets = array
return success
}

override fun getOffsetInHost(offsetInDecoded: Int, rangeInsideHost: TextRange): Int {
val offsets = outSourceOffsets ?: throw IllegalStateException("#decode was not called")
val result = if (offsetInDecoded < offsets.size) offsets[offsetInDecoded] else -1
return result.coerceIn(0..rangeInsideHost.length) + rangeInsideHost.startOffset
}

companion object {
/**
* Does not consider interpolations so that
* they do appear in the guest language and remain when we end up converting back to Nix.
*/
fun unescapeAndDecode(chars: String, outChars: StringBuilder, sourceOffsets: IntArray?): Boolean {
assert(sourceOffsets == null || sourceOffsets.size == chars.length + 1)

var index = 0
val outOffset = outChars.length
var braces = 0
var indentSoFar = 0
val minIndent = chars.lines()
.filterNot { it.isEmpty() }
.minOfOrNull { it.takeWhile(Char::isWhitespace).count() } ?: 0


while (index < chars.length) {
fun updateOffsets(index: Int) {
if (sourceOffsets != null) {
sourceOffsets[outChars.length - outOffset] = index - 1
sourceOffsets[outChars.length - outOffset + 1] = index
}
}

var c = chars[index++]
updateOffsets(index)


if (braces > 0) {
if (c == '{') braces++
else if (c == '}') braces--
outChars.append(c)
continue
}
var success = true

for (part in myHost.stringParts) {
assert(part.parent == myHost)
val partRange = part.textRangeInParent
if (partRange.startOffset >= rangeInsideHost.endOffset) {
break
} else if (partRange.endOffset < rangeInsideHost.startOffset) {
continue
}

if (c == '\n' && index < chars.length - 1) {
// we know that the next n chars are going to be whitespace indent
index += minIndent
outChars.append(c)
if (sourceOffsets != null) {
sourceOffsets[outChars.length - outOffset] = index
fun addText(text: CharSequence, offset: Int): Boolean {
val start = partRange.startOffset + offset
for (i in text.indices) {
if (start + i >= rangeInsideHost.startOffset) {
array[outChars.length - outOffset] = start + i
outChars.append(text[i])
} else if (start + i >= rangeInsideHost.endOffset) {
return false
}
continue
}
return true
}

if (c == '\'') {
if (index == chars.length) return false
c = chars[index++]

if (c != '\'') {
// if what follows isn't another ' then we are not escaping anything,
// so we can backtrace and continue
outChars.append("\'")
index--
continue
if (part is NixStringText) {
NixStringUtil.visit(object : NixStringUtil.StringVisitor {
override fun text(text: CharSequence, offset: Int): Boolean {
return addText(text, offset)
}

if (index == chars.length) return false
c = chars[index++]

when (c) {
// '' can be escaped by prefixing it with ', i.e., '''.
'\'' -> {
outChars.append("\'")
updateOffsets(index - 1)
outChars.append(c)
}
// $ can be escaped by prefixing it with '' (that is, two single quotes), i.e., ''$.
'$' -> outChars.append(c)
'\\' -> {
if (index == chars.length) return false
c = chars[index++]
when (c) {
// Linefeed, carriage-return and tab characters can
// be written as ''\n, ''\r, ''\t, and ''\ escapes any other character.
'a' -> outChars.append(0x07.toChar())
'b' -> outChars.append('\b')
'f' -> outChars.append(0x0c.toChar())
'n' -> outChars.append('\n')
't' -> outChars.append('\t')
'r' -> outChars.append('\r')
'v' -> outChars.append(0x0b.toChar())
else -> return false
override fun escapeSequence(text: String, offset: Int, escapeSequence: CharSequence): Boolean {
val start = partRange.startOffset + offset
val end = start + escapeSequence.length
return if (start < rangeInsideHost.startOffset || end > rangeInsideHost.endOffset) {
success = false
false
} else {
for (i in escapeSequence.indices) {
array[outChars.length - outOffset + i] = start
}
outChars.append(text)
true
}

else -> return false
}
if (sourceOffsets != null) {
sourceOffsets[outChars.length - outOffset] = index
}
continue
}

outChars.append(c)
}, part, maxIndent)
} else {
assert(part is NixAntiquotation)
addText(part.text, 0)
}
return true
}

outSourceOffsets = array
return success
}

override fun getOffsetInHost(offsetInDecoded: Int, rangeInsideHost: TextRange): Int {
val offsets = outSourceOffsets ?: throw IllegalStateException("#decode was not called")
val result = if (offsetInDecoded < offsets.size) offsets[offsetInDecoded] else -1
return result.coerceIn(0..rangeInsideHost.length) + rangeInsideHost.startOffset
}

}
}
59 changes: 43 additions & 16 deletions src/main/java/org/nixos/idea/util/NixStringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,55 +138,82 @@ public static int detectMaxIndent(@NotNull NixString string) {
public static @NotNull String parse(@NotNull NixStringText textNode) {
int maxIndent = detectMaxIndent((NixString) textNode.getParent());
StringBuilder builder = new StringBuilder();
visit(new StringVisitor() {
@Override
public boolean text(@NotNull CharSequence text, int offset) {
builder.append(text);
return true;
}

@Override
public boolean escapeSequence(@NotNull String text, int offset, @NotNull CharSequence escapeSequence) {
builder.append(text);
return true;
}
}, textNode, maxIndent);
return builder.toString();
}

public static void visit(@NotNull StringVisitor visitor, @NotNull NixStringText textNode, int maxIndent) {
int offset = 0;
for (ASTNode child = textNode.getNode().getFirstChildNode(); child != null; child = child.getTreeNext()) {
parse(builder, child, maxIndent);
if (!parse(visitor, child, offset, maxIndent)) {
break;
}
offset += child.getTextLength();
}
return builder.toString();
}

private static void parse(@NotNull StringBuilder builder, @NotNull ASTNode token, int maxIndent) {
private static boolean parse(@NotNull StringVisitor visitor, @NotNull ASTNode token, int offset, int maxIndent) {
CharSequence text = token.getChars();
IElementType type = token.getElementType();
if (type == NixTypes.STR || type == NixTypes.IND_STR || type == NixTypes.IND_STR_LF) {
builder.append(text);
return visitor.text(text, offset);
} else if (type == NixTypes.IND_STR_INDENT) {
int end = text.length();
if (end > maxIndent) {
CharSequence remain = text.subSequence(maxIndent, end);
builder.append(remain);
return visitor.text(remain, offset + maxIndent);
}
return true;
} else if (type == NixTypes.STR_ESCAPE) {
assert text.length() == 2 && text.charAt(0) == '\\' : text;
char c = text.charAt(1);
builder.append(unescape(c));
return visitor.escapeSequence(unescape(c), offset, text);
} else if (type == NixTypes.IND_STR_ESCAPE) {
switch (text.charAt(2)) {
return switch (text.charAt(2)) {
case '$' -> {
assert "''$".contentEquals(text) : text;
builder.append("$");
yield visitor.escapeSequence("$", offset, text);
}
case '\'' -> {
assert "'''".contentEquals(text) : text;
builder.append("''");
yield visitor.escapeSequence("''", offset, text);
}
case '\\' -> {
assert text.length() == 4 && "''\\".contentEquals(text.subSequence(0, 3)) : text;
char c = text.charAt(3);
builder.append(unescape(c));
yield visitor.escapeSequence(unescape(c), offset, text);
}
default -> throw new IllegalStateException("Unknown escape sequence: " + text);
}
};
} else {
throw new IllegalStateException("Unexpected token in string: " + token);
}
}

private static char unescape(char c) {
private static @NotNull String unescape(char c) {
return switch (c) {
case 'n' -> '\n';
case 'r' -> '\r';
case 't' -> '\t';
default -> c;
case 'n' -> "\n";
case 'r' -> "\r";
case 't' -> "\t";
default -> String.valueOf(c);
};
}

public interface StringVisitor {
boolean text(@NotNull CharSequence text, int offset);

boolean escapeSequence(@NotNull String text, int offset, @NotNull CharSequence escapeSequence);
}
}
37 changes: 0 additions & 37 deletions src/test/java/org/nixos/idea/util/NixIndStringUtilTest.java

This file was deleted.

0 comments on commit 328d644

Please sign in to comment.