From b2682bf13700d4a93f78894b5c2a396aeac8a088 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Thu, 16 Apr 2015 17:31:33 +0300 Subject: [PATCH] Move StringNodeInfo helper class to PyUtil and reuse it in PyJoinLinesHandler --- .../PyConvertTripleQuotedStringIntention.java | 64 ++------ .../python/editor/PyJoinLinesHandler.java | 139 +++--------------- .../src/com/jetbrains/python/psi/PyUtil.java | 127 ++++++++++++++++ 3 files changed, 165 insertions(+), 165 deletions(-) diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTripleQuotedStringIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTripleQuotedStringIntention.java index 903ccb9378b6..f5be2b97273a 100644 --- a/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTripleQuotedStringIntention.java +++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyConvertTripleQuotedStringIntention.java @@ -29,7 +29,7 @@ import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyBundle; import com.jetbrains.python.psi.*; -import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; +import com.jetbrains.python.psi.PyUtil.StringNodeInfo; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -49,9 +49,6 @@ import java.util.List; */ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { - public static final String TRIPLE_SINGLE_QUOTE = "'''"; - public static final String TRIPLE_DOUBLE_QUOTE = "\"\"\""; - @NotNull public String getFamilyName() { return PyBundle.message("INTN.triple.quoted.string"); @@ -69,15 +66,14 @@ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { } final int caretOffset = editor.getCaretModel().getOffset(); - final PyStringLiteralExpression string = PsiTreeUtil.getParentOfType(file.findElementAt(caretOffset), PyStringLiteralExpression.class); - if (string != null) { - final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(string, PyDocStringOwner.class); + final PyStringLiteralExpression pyString = PsiTreeUtil.getParentOfType(file.findElementAt(caretOffset), PyStringLiteralExpression.class); + if (pyString != null) { + final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(pyString, PyDocStringOwner.class); if (docStringOwner != null) { - if (docStringOwner.getDocStringExpression() == string) return false; + if (docStringOwner.getDocStringExpression() == pyString) return false; } - - for (StringNodeInfo info : extractStringNodesInfo(string)) { - if (info.isTripleQuoted && info.node.getTextRange().contains(caretOffset)) { + for (StringNodeInfo info : extractStringNodesInfo(pyString)) { + if (info.isTripleQuoted() && info.isTerminated() && info.getNode().getTextRange().contains(caretOffset)) { return true; } } @@ -94,7 +90,7 @@ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { final List nodeInfos = extractStringNodesInfo(pyString); for (int i = 0; i < nodeInfos.size(); i++) { final StringNodeInfo info = nodeInfos.get(i); - List lines = StringUtil.split(info.content, "\n", true, false); + List lines = StringUtil.split(info.getContent(), "\n", true, false); boolean lastLineExcluded = false; if (lines.size() > 1 && lines.get(lines.size() - 1).isEmpty()) { lastLineExcluded = true; @@ -106,20 +102,20 @@ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { final String line = lines.get(j); final boolean inLastLine = j == lines.size() - 1; - if (StringUtil.containsIgnoreCase(info.prefix, "r")) { + if (info.isRaw()) { appendSplittedRawStringLine(result, info, line); if (!inLastLine || lastLineExcluded) { - result.append(" ").append(info.quote).append("\\n").append(info.quote); + result.append(" ").append(info.getSingleQuote()).append("\\n").append(info.getSingleQuote()); } } else { - result.append(info.prefix); - result.append(info.quote); - result.append(convertToValidSubString(line, info.quote, info.isTripleQuoted)); + result.append(info.getPrefix()); + result.append(info.getSingleQuote()); + result.append(convertToValidSubString(line, info.getSingleQuote(), info.isTripleQuoted())); if (!inLastLine || lastLineExcluded) { result.append("\\n"); } - result.append(info.quote); + result.append(info.getSingleQuote()); } if (!(inLastNode && inLastLine)) { result.append("\n"); @@ -164,7 +160,7 @@ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { singleQuoteUsed = false; } else if (k == line.length()) { - chunkQuote = info.quote; + chunkQuote = info.getSingleQuote(); } else { continue; @@ -172,7 +168,7 @@ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { if (!firstChunk) { result.append(" "); } - result.append(info.prefix).append(chunkQuote).append(line.substring(chunkStart, k)).append(chunkQuote); + result.append(info.getPrefix()).append(chunkQuote).append(line.substring(chunkStart, k)).append(chunkQuote); firstChunk = false; chunkStart = k; } @@ -183,16 +179,6 @@ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { return isMultiline ? StringUtil.escapeChar(content, newQuote) : content; } - private static boolean isTripleQuotedString(@NotNull String text) { - final int prefixLength = PyStringLiteralExpressionImpl.getPrefixLength(text); - text = text.substring(prefixLength); - if (text.length() < 6) { - return false; - } - return (text.startsWith(TRIPLE_SINGLE_QUOTE) && text.endsWith(TRIPLE_SINGLE_QUOTE)) || - (text.startsWith(TRIPLE_DOUBLE_QUOTE) && text.endsWith(TRIPLE_DOUBLE_QUOTE)); - } - @NotNull private static List extractStringNodesInfo(@NotNull PyStringLiteralExpression expression) { return ContainerUtil.map(expression.getStringNodes(), new Function() { @@ -202,22 +188,4 @@ public class PyConvertTripleQuotedStringIntention extends BaseIntentionAction { } }); } - - private static class StringNodeInfo { - final ASTNode node; - final String prefix; - final String content; - final char quote; - final boolean isTripleQuoted; - - public StringNodeInfo(@NotNull ASTNode node) { - this.node = node; - final String nodeText = node.getText(); - final int prefixLength = PyStringLiteralExpressionImpl.getPrefixLength(nodeText); - prefix = nodeText.substring(0, prefixLength); - content = PyStringLiteralExpressionImpl.getNodeTextRange(nodeText).substring(nodeText); - quote = nodeText.charAt(prefixLength); - isTripleQuoted = isTripleQuotedString(nodeText.substring(prefixLength)); - } - } } diff --git a/python/src/com/jetbrains/python/editor/PyJoinLinesHandler.java b/python/src/com/jetbrains/python/editor/PyJoinLinesHandler.java index dac60d10684d..4f600cd6d9c0 100644 --- a/python/src/com/jetbrains/python/editor/PyJoinLinesHandler.java +++ b/python/src/com/jetbrains/python/editor/PyJoinLinesHandler.java @@ -29,7 +29,7 @@ import com.jetbrains.python.psi.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Locale; +import static com.jetbrains.python.psi.PyUtil.StringNodeInfo; /** * Joins lines sanely. @@ -228,24 +228,24 @@ public class PyJoinLinesHandler implements JoinRawLinesHandlerDelegate { (req.leftExpr instanceof PyStringLiteralExpression && req.rightExpr instanceof PyStringLiteralExpression)) { // two quoted strings close by final CharSequence text = req.document.getCharsSequence(); - final StrMod leftMod = new StrMod(text, req.leftElem.getTextRange()); - final StrMod rightMod = new StrMod(text, req.rightElem.getTextRange()); - if (leftMod.isOk() && rightMod.isOk()) { - final String lquo = leftMod.quote(); - if (leftMod.equals(rightMod)) { - return new Result("", 0, lquo.length(), rightMod.getStartPadding()); + final StringNodeInfo leftNodeInfo = new StringNodeInfo(req.leftElem); + final StringNodeInfo rightNodeInfo = new StringNodeInfo(req.rightElem); + if (leftNodeInfo.isTerminated() && rightNodeInfo.isTerminated()) { + final int rightNodeContentOffset = rightNodeInfo.getContentRange().getStartOffset(); + if (leftNodeInfo.equals(rightNodeInfo)) { + return new Result("", 0, leftNodeInfo.getQuote().length(), rightNodeContentOffset); } - else if (leftMod.compatibleTo(rightMod) && lquo.length() == 1 && rightMod.quote().length() == 1) { + if (haveSamePrefixes(leftNodeInfo, rightNodeInfo) && !leftNodeInfo.isTripleQuoted() && !rightNodeInfo.isTripleQuoted()) { // maybe fit one literal's quotes to match other's - if (!containsChar(text, rightMod.getInnerRange(), leftMod.quote().charAt(0))) { - final int quotePos = rightMod.getInnerRange().getEndOffset(); - req.document.replaceString(quotePos, quotePos + 1, leftMod.quote()); - return new Result("", 0, leftMod.quote().length(), rightMod.getStartPadding()); + if (!rightNodeInfo.getContent().contains(leftNodeInfo.getQuote())) { + final int quotePos = rightNodeInfo.getAbsoluteContentRange().getEndOffset(); + req.document.replaceString(quotePos, quotePos + 1, leftNodeInfo.getQuote()); + return new Result("", 0, 1, rightNodeContentOffset); } - else if (!containsChar(text, leftMod.getInnerRange(), rightMod.quote().charAt(0))) { - final int quotePos = leftMod.getInnerRange().getStartOffset() - 1; - req.document.replaceString(quotePos, quotePos + 1, rightMod.quote()); - return new Result("", 0, leftMod.quote().length(), rightMod.getStartPadding()); + else if (!leftNodeInfo.getContent().contains(rightNodeInfo.getQuote())) { + final int quotePos = leftNodeInfo.getAbsoluteContentRange().getStartOffset() - 1; + req.document.replaceString(quotePos, quotePos + 1, rightNodeInfo.getQuote()); + return new Result("", 0, 1, rightNodeContentOffset); } } } @@ -254,109 +254,14 @@ public class PyJoinLinesHandler implements JoinRawLinesHandlerDelegate { return null; } - protected static boolean containsChar(@NotNull CharSequence text, @NotNull TextRange range, char c) { - return StringUtil.contains(text, range.getStartOffset(), range.getEndOffset(), c); + private static boolean haveSamePrefixes(@NotNull StringNodeInfo leftNodeInfo, @NotNull StringNodeInfo rightNodeInfo) { + return leftNodeInfo.isUnicode() == rightNodeInfo.isUnicode() && + leftNodeInfo.isRaw() == rightNodeInfo.isRaw() && + leftNodeInfo.isBytes() == rightNodeInfo.isBytes(); } - - private static class StrMod { - @NotNull private final String myPrefix; // "u", "b", or "" - private final boolean myRaw; // is raw or not - @NotNull private final String myQuote; // single or double, one or triple. - - @Nullable private final TextRange myInnerRange; - - public StrMod(@NotNull CharSequence text, @NotNull TextRange range) { - int pos = range.getStartOffset(); - char c = text.charAt(pos); - if ("Uu".indexOf(c) > -1 || "Bb".indexOf(c) > -1) { - myPrefix = String.valueOf(c).toLowerCase(Locale.US); - pos += 1; - c = text.charAt(pos); - } - else { - myPrefix = ""; - } - if ("Rr".indexOf(c) > -1) { - myRaw = true; - pos += 1; - c = text.charAt(pos); - } - else { - myRaw = false; - } - final char quote = c; - if ("'\"".indexOf(quote) < 0) { - myInnerRange = null; - myQuote = ""; - return; // failed to find a quote - } - // TODO: we could run a simple but complete parser here, only checking escapes - if (range.getLength() >= 6 && text.charAt(pos + 1) == quote && text.charAt(pos + 2) == quote) { - myQuote = text.subSequence(pos, pos + 3).toString(); - if (!myQuote.equals(text.subSequence(range.getEndOffset() - 3, range.getEndOffset()).toString())) { - myInnerRange = null; - return; - } - } - else { - myQuote = text.subSequence(pos, pos + 1).toString(); - if (!myQuote.equals(text.subSequence(range.getEndOffset() - 1, range.getEndOffset()).toString())) { - myInnerRange = null; - return; - } - } - myInnerRange = TextRange.from(range.getStartOffset() + getStartPadding(), range.getLength() - getStartPadding() - quote().length()); - } - - public boolean isOk() { - return myInnerRange != null; - } - - @NotNull - public String prefix() { - return myPrefix; - } - - @NotNull - public String quote() { - return myQuote; - } - - public boolean isRaw() { - return myRaw; - } - - @Override - public boolean equals(Object o) { - if (o instanceof StrMod) { - final StrMod other = (StrMod)o; - return compatibleTo(other) && myQuote.equals(other.quote()); - } - return false; - } - - /** - * @return combined length of initial modifier letters and opening quotes - */ - public int getStartPadding() { - return myQuote.length() + myPrefix.length() + (myRaw ? 1 : 0); - } - - /** - * @param other - * @return true iff this and other have the same byte/unicode and raw prefixes. - */ - public boolean compatibleTo(@NotNull StrMod other) { - return isOk() && other.isOk() && myRaw == other.isRaw() && myPrefix.equals(other.prefix()); - } - /** - * @return range of text part inside quotes - */ - @Nullable - public TextRange getInnerRange() { - return myInnerRange; - } + protected static boolean containsChar(@NotNull CharSequence text, @NotNull TextRange range, char c) { + return StringUtil.contains(text, range.getStartOffset(), range.getEndOffset(), c); } } diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index 00da7348c8b2..4d104f1c2629 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -38,6 +38,7 @@ import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; @@ -66,6 +67,7 @@ import com.jetbrains.python.codeInsight.stdlib.PyNamedTupleType; import com.jetbrains.python.magicLiteral.PyMagicLiteralTools; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyPsiUtils; +import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.resolve.QualifiedNameFinder; @@ -1800,4 +1802,129 @@ public class PyUtil { final PyBuiltinCache builtinCache = PyBuiltinCache.getInstance(anchor); return type == builtinCache.getObjectType() || type == builtinCache.getOldstyleClassobjType(); } + + /** + * This helper class allows to collect various information about AST nodes composing {@link PyStringLiteralExpression}. + */ + public static final class StringNodeInfo { + private final ASTNode myNode; + private final String myPrefix; + private final String myQuote; + private final TextRange myContentRange; + + public StringNodeInfo(@NotNull ASTNode node) { + if (!PyTokenTypes.STRING_NODES.contains(node.getElementType())) { + throw new IllegalArgumentException("Node must be valid Python string literal token, but " + node.getElementType() + " was given"); + } + myNode = node; + final String nodeText = node.getText(); + final int prefixLength = PyStringLiteralExpressionImpl.getPrefixLength(nodeText); + myPrefix = nodeText.substring(0, prefixLength); + myContentRange = PyStringLiteralExpressionImpl.getNodeTextRange(nodeText); + myQuote = nodeText.substring(prefixLength, myContentRange.getStartOffset()); + } + + public StringNodeInfo(@NotNull PsiElement element) { + this(element.getNode()); + } + + @NotNull + public ASTNode getNode() { + return myNode; + } + + /** + * @return string prefix, e.g. "UR", "b" etc. + */ + @NotNull + public String getPrefix() { + return myPrefix; + } + + /** + * @return content of the string node between quotes + */ + @NotNull + public String getContent() { + return myContentRange.substring(myNode.getText()); + } + + /** + * @return relative range of the content (excluding prefix and quotes) + * @see #getAbsoluteContentRange() + */ + @NotNull + public TextRange getContentRange() { + return myContentRange; + } + + /** + * @return absolute content range that accounts offset of the {@link #getNode() node} in the document + */ + @NotNull + public TextRange getAbsoluteContentRange() { + return getContentRange().shiftRight(myNode.getStartOffset()); + } + + /** + * @return the first character of {@link #getQuote()} + */ + public char getSingleQuote() { + return myQuote.charAt(0); + } + + @NotNull + public String getQuote() { + return myQuote; + } + + public boolean isTripleQuoted() { + return myQuote.length() == 3; + } + + /** + * @return true if string literal ends with starting quote + */ + public boolean isTerminated() { + final String text = myNode.getText(); + return text.length() - myPrefix.length() >= myQuote.length() * 2 && text.endsWith(myQuote); + } + + /** + * @return true if given string node contains "u" or "U" prefix + */ + public boolean isUnicode() { + return StringUtil.containsIgnoreCase(myPrefix, "u"); + } + + /** + * @return true if given string node contains "r" or "R" prefix + */ + public boolean isRaw() { + return StringUtil.containsIgnoreCase(myPrefix, "r"); + } + + /** + * @return true if given string node contains "b" or "B" prefix + */ + public boolean isBytes() { + return StringUtil.containsIgnoreCase(myPrefix, "b"); + } + + /** + * @return true if other string node has the same decorations, i.e. quotes and prefix + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StringNodeInfo info = (StringNodeInfo)o; + + return getQuote().equals(info.getQuote()) && + isRaw() == info.isRaw() && + isUnicode() == info.isUnicode() && + isBytes() == info.isBytes(); + } + } }