String (@NotNull StringKind kind, @NotNull String s) { if (s.isEmpty()) { return s; } if (kind != StringKind.DOUBLE_QUOTED && kind != StringKind.TRIPLE_DOUBLE_QUOTED) { return kind.escape(s); } boolean singleLine = kind == StringKind.DOUBLE_QUOTED; StringBuilder b = new StringBuilder(s.length()); GrStringUtil.escapeStringCharacters(s.length(), s, singleLine ? "\"" : "", singleLine, true, b); GrStringUtil.unescapeCharacters(b, singleLine ? "'" : "'\"", true); for (int i = b.length() - 2; i >= 0; i--) { if (b.charAt(i) == '$') { final char next = b.charAt(i + 1); if (next != '{' && !Character.isLetter(next)) { b.insert(i, '\\'); } } } if (b.charAt(b.length() - 1) == '$') { b.insert(b.length() - 1, '\\'); } return b.toString(); } @VisibleForTesting @Nullable public static StringKind findStringKind(PsiFile file, int startOffset, int endOffset) { if (startOffset == endOffset) { return findStringKind(file, startOffset); } StringKind startKind = findStringKind(file, startOffset); StringKind endKind = findStringKind(file, endOffset); if (startKind == endKind) { return startKind; } return null; } @Nullable private static StringKind findStringKind(@NotNull PsiFile file, int offset) { final PsiElement leaf = file.findElementAt(offset); if (leaf == null) { return null; } final IElementType leafType = leaf.getNode().getElementType(); final int leafOffset = leaf.getTextOffset(); if (offset == leafOffset) { // special cases if (leafType == GSTRING_END || leafType == SLASHY_END || leafType == DOLLAR_SLASHY_END) { PsiElement prevSibling = leaf.getPrevSibling(); if (prevSibling != null) { IElementType previousElementType = prevSibling.getNode().getElementType(); if (previousElementType == STRING_INJECTION) { if (leafType == GSTRING_END) { if (isMultiline(leaf.getParent())) { // """...${}<here>""" return StringKind.TRIPLE_DOUBLE_QUOTED; } else { // "...${}<here>" return StringKind.DOUBLE_QUOTED; } } if (leafType == SLASHY_END) { // /...${}<here>/ return StringKind.SLASHY; } else { // $/...${}<here>/$ return StringKind.DOLLAR_SLASHY; } } else if (previousElementType == STRING_CONTENT) { // "...${}...<here>" // """...${}...<here>""" // /...${}...<here>/ // $/...${}...<here>/$ PsiElement contentLeaf = prevSibling.getFirstChild(); return getStringKindByContentTokenType(contentLeaf, contentLeaf.getNode().getElementType()); } else if (previousElementType == DOLLAR_SLASHY_BEGIN) { // $/<here>/$ return StringKind.DOLLAR_SLASHY; } else if (leafType != GSTRING_END) { // /...<here>/ // $/...<here>/$ // // "..." is a single token returned by lexer, // while slashy and dollar slashy strings without injections consist of three tokens start quote, content and end quote return getStringKindByContentTokenType(leaf, previousElementType); } } } else if (leafType == T_DOLLAR) { // Strings with injections, leaf is dollar: // "<here>${}..." or "...<here>${}..." // /<here>${}.../ or /...<here>${}.../ // $/<here>${}.../$ or $/...<here>${}.../$ PsiElement parent = leaf.getParent(); // template string injection PsiElement gParent = parent.getParent(); // template string element; return getStringKindByStringElement(gParent); } } final int leafEndOffset = leafOffset + leaf.getTextLength(); if (leafType == STRING_SQ || leafType == STRING_DQ) { if (leafOffset + 1 <= offset && offset <= leafEndOffset - 1) { // Single token literals: // '<here>......' // '...<here>...' // '......<here>' // "<here>......" // "...<here>..." // "......<here>" return leafType == STRING_SQ ? StringKind.SINGLE_QUOTED : StringKind.DOUBLE_QUOTED; } } if (leafType == STRING_TSQ || leafType == STRING_TDQ) { // Single token literals: // '''<here>...''' // """<here>...""" if (leafOffset + 3 <= offset && offset <= leafEndOffset - 3) { return leafType == STRING_TSQ ? StringKind.TRIPLE_SINGLE_QUOTED : StringKind.TRIPLE_DOUBLE_QUOTED; } } // Strings with injections, leaf is string content: // "<here>...${}..." or "...<here>...${}..." // """<here>...${}...""" or """...<here>...${}...""" // /<here>...${}.../ or /...<here>...${}.../ // $/<here>...${}.../$ or $/...<here>...${}.../$ return getStringKindByContentTokenType(leaf, leafType); } @Nullable private static StringKind getStringKindByStringElement(@NotNull PsiElement templateStringElement) { IElementType elementType = templateStringElement.getNode().getElementType(); if (elementType == GSTRING) { return isMultiline(templateStringElement) ? StringKind.TRIPLE_DOUBLE_QUOTED : StringKind.DOUBLE_QUOTED; } else if (elementType == REGEX) { IElementType quoteType = templateStringElement.getFirstChild().getNode().getElementType(); return quoteType == SLASHY_BEGIN ? StringKind.SLASHY : StringKind.DOLLAR_SLASHY; } else if (elementType == SLASHY_LITERAL) { return StringKind.SLASHY; } else if (elementType == DOLLAR_SLASHY_LITERAL) { return StringKind.DOLLAR_SLASHY; } else { return null; } } @Nullable private static StringKind getStringKindByContentTokenType(PsiElement leaf, IElementType contentTokenType) { if (contentTokenType == GSTRING_CONTENT) { PsiElement parent = leaf.getParent(); // template string content PsiElement gParent = parent.getParent(); // template string element if (isMultiline(gParent)) { // "....${}...<here>" return StringKind.TRIPLE_DOUBLE_QUOTED; } else { // """....${}...<here>""" return StringKind.DOUBLE_QUOTED; } } else if (contentTokenType == SLASHY_CONTENT) { // /....${}...<here>/ return StringKind.SLASHY; } else if (contentTokenType == DOLLAR_SLASHY_CONTENT) { // $/...${}...<here>/$ return StringKind.DOLLAR_SLASHY; } return null; } private static boolean isMultiline(@NotNull PsiElement templateStringElement) { return templateStringElement.getFirstChild().textMatches("\"\"\""); } @Nullable private static StringKind getStringKindByToken(@NotNull PsiElement token) { IElementType leafType = token.getNode().getElementType(); if (leafType == STRING_SQ) { return StringKind.SINGLE_QUOTED; } else if (leafType == STRING_DQ) { return StringKind.DOUBLE_QUOTED; } else if (leafType == STRING_TSQ) { return StringKind.TRIPLE_SINGLE_QUOTED; } else if (leafType == STRING_TDQ) { return StringKind.TRIPLE_DOUBLE_QUOTED; } return getStringKindByContentTokenType(token, leafType); } @Override protected String getLineBreaker(@NotNull PsiElement token) { throw new IllegalStateException("must not be called"); } @NotNull @Override protected String escapeCharCharacters(@NotNull String s, @NotNull PsiElement token) { throw new IllegalStateException("must not be called"); } @Override public String escapeAndSplit(String text, PsiElement token) { throw new IllegalStateException("must not be called"); } @NotNull @Override protected String escapeTextBlock(@NotNull String text, int offset, boolean escapeStartQuote, boolean escapeEndQuote) { throw new IllegalStateException("must not be called"); } @Override protected boolean isCharLiteral(@NotNull PsiElement token) { throw new IllegalStateException("must not be called"); } @Override protected boolean isStringLiteral(@NotNull PsiElement token) { throw new IllegalStateException("must not be called"); } @Override protected boolean isTextBlock(@NotNull PsiElement token) { throw new IllegalStateException("must not be called"); } @Override @Nullable protected PsiElement findLiteralTokenType(PsiFile file, int selectionStart, int selectionEnd) { throw new IllegalStateException("must not be called"); } }