PY-12205 Make string joiner and comment joiner respect maximum line length

This commit is contained in:
Valentina Kiryushkina
2016-03-01 18:58:52 +03:00
parent a2f3ff721e
commit 273fc6e6d5
10 changed files with 144 additions and 21 deletions
@@ -22,13 +22,17 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static com.jetbrains.python.psi.PyUtil.StringNodeInfo;
/**
@@ -226,43 +230,100 @@ public class PyJoinLinesHandler implements JoinRawLinesHandlerDelegate {
final PsiElement parent = req.rightElem.getParent();
if ((req.leftElem.getParent() == parent && parent instanceof PyStringLiteralExpression) ||
(req.leftExpr instanceof PyStringLiteralExpression && req.rightExpr instanceof PyStringLiteralExpression)) {
// two quoted strings close by
final CharSequence text = req.document.getCharsSequence();
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();
int quotesMaxLength = Math.max(leftNodeInfo.getQuote().length(), rightNodeInfo.getQuote().length());
int stringToJoinMaxLength = getStringToJoinMaxLength(req, quotesMaxLength);
final String replacement = findReplacement(rightNodeInfo.getContent(), stringToJoinMaxLength);
if (leftNodeInfo.equals(rightNodeInfo)) {
return new Result("", 0, leftNodeInfo.getQuote().length(), rightNodeContentOffset);
}
if (haveSamePrefixes(leftNodeInfo, rightNodeInfo) && !leftNodeInfo.isTripleQuoted() && !rightNodeInfo.isTripleQuoted()) {
// maybe fit one literal's quotes to match other's
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 (!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);
}
return getResultAndSplitStringIfTooLong(req, leftNodeInfo, rightNodeInfo, replacement, leftNodeInfo.getQuote());
}
return processStringsWithDifferentQuotes(req, leftNodeInfo, rightNodeInfo, replacement);
}
}
}
return null;
}
@Nullable
private static Result processStringsWithDifferentQuotes(@NotNull final Request req,
@NotNull final StringNodeInfo leftNodeInfo,
@NotNull final StringNodeInfo rightNodeInfo,
@NotNull final String replacement) {
if (haveSamePrefixes(leftNodeInfo, rightNodeInfo) && !leftNodeInfo.isTripleQuoted() && !rightNodeInfo.isTripleQuoted()) {
if (!rightNodeInfo.getContent().contains(leftNodeInfo.getQuote())) {
final int quotePos = rightNodeInfo.getAbsoluteContentRange().getEndOffset();
final String quote = leftNodeInfo.getQuote();
req.document.replaceString(quotePos, quotePos + 1, quote);
return getResultAndSplitStringIfTooLong(req, leftNodeInfo, rightNodeInfo, replacement, quote);
}
else if (!leftNodeInfo.getContent().contains(rightNodeInfo.getQuote())) {
final int quotePos = leftNodeInfo.getAbsoluteContentRange().getStartOffset() - 1;
final String quote = rightNodeInfo.getQuote();
req.document.replaceString(quotePos, quotePos + 1, quote);
return getResultAndSplitStringIfTooLong(req, leftNodeInfo, rightNodeInfo, replacement, quote);
}
}
return null;
}
@NotNull
private static Result getResultAndSplitStringIfTooLong(@NotNull final Request req,
@NotNull final StringNodeInfo leftNodeInfo,
@NotNull final StringNodeInfo rightNodeInfo,
@NotNull final String replacement,
@NotNull final String quote) {
int cutIntoRight = rightNodeInfo.getContentRange().getStartOffset();
String lineEnd = "";
if (!replacement.isEmpty()) {
cutIntoRight = replacement.length() + rightNodeInfo.getQuote().length();
int contentWithQuoteStartColumn = leftNodeInfo.getAbsoluteContentRange().getStartOffset() - getLeftLineStartOffset(req)
- leftNodeInfo.getQuote().length();
int quotePos = rightNodeInfo.getAbsoluteContentRange().getStartOffset() + replacement.length();
req.document.insertString(quotePos, rightNodeInfo.getQuote());
req.document.insertString(quotePos, StringUtil.repeat(" ", contentWithQuoteStartColumn));
lineEnd = quote + "\\\n";
}
return new Result(replacement + lineEnd, 0, leftNodeInfo.getQuote().length(), cutIntoRight);
}
private static boolean haveSamePrefixes(@NotNull StringNodeInfo leftNodeInfo, @NotNull StringNodeInfo rightNodeInfo) {
return leftNodeInfo.isUnicode() == rightNodeInfo.isUnicode() &&
leftNodeInfo.isRaw() == rightNodeInfo.isRaw() &&
leftNodeInfo.isBytes() == rightNodeInfo.isBytes();
}
}
protected static boolean containsChar(@NotNull CharSequence text, @NotNull TextRange range, char c) {
return StringUtil.contains(text, range.getStartOffset(), range.getEndOffset(), c);
private static String findReplacement(String text, int maxLength) {
if (text.length() < maxLength) return "";
List<String> words = StringUtil.split(text, " ");
StringBuilder builder = new StringBuilder();
final int delimiterLength = 1;
int wordsLength = 0;
for (String word: words) {
wordsLength += word.length() + delimiterLength;
if (wordsLength >= maxLength) break;
builder.append(word);
builder.append(" ");
}
return builder.toString();
}
private static int getStringToJoinMaxLength(Request request, int symbolsToSkip) {
int leftLineStartOffset = getLeftLineStartOffset(request);
final int margin = CodeStyleSettingsManager.getInstance().getCurrentSettings().getRightMargin(PythonLanguage.getInstance());
int leftLineLength = request.document.getLineEndOffset(request.document.getLineNumber(leftLineStartOffset)) - leftLineStartOffset;
return margin - leftLineLength - symbolsToSkip;
}
private static int getLeftLineStartOffset(@NotNull Request req) {
int lineNumber = req.document.getLineNumber(req.firstLineEndOffset);
return req.document.getLineStartOffset(lineNumber);
}
private static class CommentJoiner implements Joiner {
@@ -275,11 +336,22 @@ public class PyJoinLinesHandler implements JoinRawLinesHandlerDelegate {
int pos = initialPos; // cut '#'
final int last = rightRange.getEndOffset();
while (pos < last && " \t".indexOf(text.charAt(pos)) >= 0) pos += 1;
final int right = pos - initialPos + 1; // account for the '#'
return new Result(" ", 0, 0, right);
int right = pos - initialPos + 1; // account for the '#'
String substring = req.rightElem.getText().substring(right);
String replacement = " " + findReplacement(substring, getStringToJoinMaxLength(req, 0));
right += replacement.length() - 1; // account for the '#'
if (!replacement.trim().isEmpty()) {
replacement += "\n";
req.document.insertString(req.secondLineStartOffset + right, "# ");
}
return new Result(replacement, 0, 0, right);
}
return null;
}
}
private static class StripBackslashJoiner implements Joiner {
@@ -0,0 +1,4 @@
# this comment is very very very very very very long. And this is the second
# line of this very long comment
def test():
pass
@@ -0,0 +1,4 @@
# this comment is very very very very very very long.
# And this is the second line of this very long comment
def test():
pass
@@ -0,0 +1,2 @@
s = "first line is very very very very very very very long so the second "\
"is long enough"
@@ -0,0 +1,2 @@
s = "first line is very very very very very very very long "\
"so the second is long enough"
@@ -0,0 +1,2 @@
s = "first line is very very very very very very very long it's line with "\
"quote"
@@ -0,0 +1,2 @@
s = 'first line is very very very very very very very long '\
"it's line with quote"
@@ -0,0 +1,2 @@
s = '''first line is very very very very long it's line with long '''\
'''long long long line'''
@@ -0,0 +1,2 @@
s = '''first line is very very very very long it's line with ''' \
'''long long long long line'''
@@ -16,6 +16,8 @@
package com.jetbrains.python;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.jetbrains.python.fixtures.PyTestCase;
/**
@@ -30,6 +32,14 @@ public class PyJoinLinesTest extends PyTestCase {
myFixture.checkResultByFile("joinLines/" + getTestName(false) + "-after.py");
}
private void doTestWithCodeStyleSettings() {
CodeStyleSettings settings = new CodeStyleSettings();
settings.setRightMargin(PythonLanguage.getInstance(), 79);
CodeStyleSettingsManager.getInstance().setTemporarySettings(settings);
doTest();
}
public void testBinaryOpBelow() {
doTest();
}
@@ -139,6 +149,27 @@ public class PyJoinLinesTest extends PyTestCase {
doTest();
}
//PY-12205
public void testStringsProducesTooLongLineAfterJoin() {
doTestWithCodeStyleSettings();
}
//PY-12205
public void testStringsWithDifferentQuotesTooLongToJoin() {
doTestWithCodeStyleSettings();
}
//PY-12205
public void testStringsWithTripleQuotesTooLongToJoin() {
doTestWithCodeStyleSettings();
}
//PY-12205
public void testCommentProducesTooLongLineAfterJoin() {
doTestWithCodeStyleSettings();
}
// PY-15564
public void testBackslashBetweenTargetsInImport() {
doTest();