Merge remote-tracking branch 'origin/master'

This commit is contained in:
Yann Cébron
2015-04-16 20:27:57 +02:00
3 changed files with 165 additions and 165 deletions
@@ -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<StringNodeInfo> nodeInfos = extractStringNodesInfo(pyString);
for (int i = 0; i < nodeInfos.size(); i++) {
final StringNodeInfo info = nodeInfos.get(i);
List<String> lines = StringUtil.split(info.content, "\n", true, false);
List<String> 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<StringNodeInfo> extractStringNodesInfo(@NotNull PyStringLiteralExpression expression) {
return ContainerUtil.map(expression.getStringNodes(), new Function<ASTNode, StringNodeInfo>() {
@@ -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));
}
}
}
@@ -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);
}
}
@@ -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 <em>relative</em> range of the content (excluding prefix and quotes)
* @see #getAbsoluteContentRange()
*/
@NotNull
public TextRange getContentRange() {
return myContentRange;
}
/**
* @return <em>absolute</em> 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();
}
}
}