PY-12360 More robust way to handle trailing continuation backslashes

Python lexer and parser can split whitespaces and attach them to different parents,
so we have to scan backward through consequtive leaf elements to collect all of them.

I also added several new tests for various such corner cases.
This commit is contained in:
Mikhail Golubev
2015-04-27 20:34:49 +03:00
parent aa43e476c2
commit 09f918bf28
11 changed files with 101 additions and 41 deletions
@@ -19,18 +19,16 @@ import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.psi.PyElementGenerator;
import com.jetbrains.python.psi.PyFile;
@@ -38,7 +36,9 @@ import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.jetbrains.python.psi.PyUtil.as;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Strip trailing extra blank lines at the end of the file and insert necessary line feed if corresponding whitespace element belongs to
@@ -55,8 +55,10 @@ public class PyTrailingBlankLinesPostFormatProcessor implements PostFormatProces
public PsiElement processElement(@NotNull PsiElement source, @NotNull CodeStyleSettings settings) {
if (source instanceof PyFile) {
final PyFile pyFile = (PyFile)source;
final Couple<PsiWhiteSpace> range = findTrailingWhitespaces(pyFile);
if (range != null && PsiTreeUtil.isAncestor(source, range.getFirst(), false)) {
final List<PsiElement> range = findTrailingWhitespaces(pyFile);
if (!range.isEmpty() &&
PsiTreeUtil.isAncestor(source, ContainerUtil.getFirstItem(range), false) &&
PsiTreeUtil.isAncestor(source, ContainerUtil.getLastItem(range), false)) {
replaceOrDeleteTrailingWhitespaces(pyFile, range);
}
}
@@ -68,9 +70,8 @@ public class PyTrailingBlankLinesPostFormatProcessor implements PostFormatProces
if (!(source instanceof PyFile)) {
return rangeToReformat;
}
final Couple<PsiWhiteSpace> range = findTrailingWhitespaces(source);
final TextRange oldWhitespaceRange;
oldWhitespaceRange = range != null ? unionRange(range) : TextRange.from(source.getTextLength(), 0);
final List<PsiElement> range = findTrailingWhitespaces(source);
final TextRange oldWhitespaceRange = !range.isEmpty() ? unionRange(range) : TextRange.from(source.getTextLength(), 0);
if (rangeToReformat.intersects(oldWhitespaceRange)) {
final TextRange newWhitespaceRange = replaceOrDeleteTrailingWhitespaces((PyFile)source, range);;
final int delta = newWhitespaceRange.getLength() - oldWhitespaceRange.getLength();
@@ -93,27 +94,46 @@ public class PyTrailingBlankLinesPostFormatProcessor implements PostFormatProces
return rangeToReformat;
}
@Nullable
private static Couple<PsiWhiteSpace> findTrailingWhitespaces(@NotNull PsiFile file) {
final PsiWhiteSpace lastWhitespace = as(PsiTreeUtil.lastChild(file), PsiWhiteSpace.class);
if (lastWhitespace == null) {
return null;
@NotNull
private static List<PsiElement> findTrailingWhitespaces(@NotNull PsiFile file) {
final PsiElement lastLeaf = PsiTreeUtil.lastChild(file);
if (isWhitespaceBackslashOrEmptyError(lastLeaf)) {
final List<PsiElement> result = new ArrayList<PsiElement>();
boolean containsWhitespaces = false;
for (PsiElement prev = lastLeaf; isWhitespaceBackslashOrEmptyError(prev); prev = PsiTreeUtil.prevLeaf(prev)) {
containsWhitespaces |= prev instanceof PsiWhiteSpace;
result.add(prev);
}
if (containsWhitespaces) {
Collections.reverse(result);
return result;
}
}
PsiWhiteSpace firstWhitespace = lastWhitespace;
for (PsiElement prev = lastWhitespace.getPrevSibling(); prev instanceof PsiWhiteSpace; prev = prev.getPrevSibling()) {
firstWhitespace = (PsiWhiteSpace)prev;
return Collections.emptyList();
}
private static boolean isWhitespaceBackslashOrEmptyError(@Nullable PsiElement elem) {
if (elem == null) {
return false;
}
return Couple.of(firstWhitespace, lastWhitespace);
return elem instanceof PsiWhiteSpace ||
elem.getNode().getElementType() == PyTokenTypes.BACKSLASH ||
(elem instanceof PsiErrorElement && elem.getTextLength() == 0);
}
@NotNull
private static TextRange unionRange(@NotNull Couple<? extends PsiElement> range) {
return range.getFirst().getTextRange().union(range.getSecond().getTextRange());
private static TextRange unionRange(@NotNull List<PsiElement> range) {
if (range.isEmpty()) {
return TextRange.EMPTY_RANGE;
}
//noinspection ConstantConditions
return ContainerUtil.getFirstItem(range).getTextRange().union(ContainerUtil.getLastItem(range).getTextRange());
}
@NotNull
private static TextRange replaceOrDeleteTrailingWhitespaces(@NotNull final PyFile pyFile,
@Nullable final Couple<PsiWhiteSpace> whitespaces) {
@NotNull final List<PsiElement> whitespaces) {
final Project project = pyFile.getProject();
final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
final Document document = documentManager.getDocument(pyFile);
@@ -134,7 +154,7 @@ public class PyTrailingBlankLinesPostFormatProcessor implements PostFormatProces
return codeStyleManager.performActionWithFormatterDisabled(new Computable<TextRange>() {
@Override
public TextRange compute() {
if (whitespaces != null) {
if (!whitespaces.isEmpty()) {
return replaceOrDeletePsiRange(whitespaces, lineFeeds).getTextRange();
}
else {
@@ -143,7 +163,7 @@ public class PyTrailingBlankLinesPostFormatProcessor implements PostFormatProces
}
});
}
else if (whitespaces != null) {
else if (!whitespaces.isEmpty()) {
return codeStyleManager.performActionWithFormatterDisabled(new Computable<TextRange>() {
@Override
public TextRange compute() {
@@ -153,26 +173,19 @@ public class PyTrailingBlankLinesPostFormatProcessor implements PostFormatProces
});
}
}
return whitespaces == null ? TextRange.from(pyFile.getTextLength(), 0) : unionRange(whitespaces);
return whitespaces.isEmpty() ? TextRange.from(pyFile.getTextLength(), 0) : unionRange(whitespaces);
}
@Contract("_, null -> null; _, !null -> !null")
@Nullable
private static PsiElement replaceOrDeletePsiRange(@NotNull Couple<? extends PsiElement> range, @Nullable PsiElement replacement) {
final PsiElement first = range.getFirst();
final PsiElement last = range.getSecond();
final PsiElement parent = first.getParent();
final PsiElement beforeFirst = first.getPrevSibling();
assert !(beforeFirst instanceof PsiWhiteSpace);
parent.deleteChildRange(first, last);
// chain `first.getParent().deleteRange(first.getNextSibling(), last); first.replace(replacement)` doesn't work
private static PsiElement replaceOrDeletePsiRange(@NotNull List<PsiElement> range, @Nullable PsiElement replacement) {
final PsiElement file = range.get(0).getContainingFile();
// If whitespaces span several parents, the safest option is to append new whitespace at the end of the file
for (PsiElement element : ContainerUtil.reverse(range)) {
element.delete();
}
if (replacement != null) {
if (beforeFirst != null) {
return parent.addAfter(replacement, beforeFirst);
}
else {
return parent.add(replacement);
}
return file.add(replacement);
}
return null;
}
@@ -0,0 +1,5 @@
def foo():
pass
\
\
\
@@ -0,0 +1,2 @@
def foo():
pass
@@ -0,0 +1,4 @@
def foo():
pass \
\
\
@@ -0,0 +1,4 @@
def foo():
pass \
\
\
@@ -0,0 +1,2 @@
def foo():
pass
@@ -0,0 +1,6 @@
def foo():
pass \
\
\
@@ -0,0 +1,2 @@
def foo():
pass
@@ -1,2 +1,2 @@
id = 1 if looooooooooooooooooooooooong_vaaaaaaaaaaaaaaaar == 'loooooooooooooooong_vaaaaaaaaaaaaaaaaaaaaaaaaaalue' else \
list('foo')[0]
list('foo')[0]
@@ -542,6 +542,26 @@ public class PyFormatterTest extends PyTestCase {
doTest();
}
// PY-11552
public void testTrailingBlankLinesWithBackslashesAtFileEnd() {
doTest();
}
// PY-11552
public void testTrailingBlankLinesWithBackslashesAtFunctionEnd() {
doTest();
}
// PY-11552
public void testTrailingBlankLinesWithBackslashesAtFunctionEndNoNewLine() {
doTest();
}
// PY-11552
public void testTrailingBlankLinesWithBackslashesMixed() {
doTest();
}
// PY-15530
public void testAlignmentInArgumentListWhereFirstArgumentIsEmptyCall() {
doTest();