mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
+16
-48
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user