PY-20138 Use existing indent of pasted fragment if caret is at first column

Unless this indentation is going to break existing block structure,
e.g. by splitting the containing function in the middle.
Additionally I improved detection of an empty statement list when the
caret is at the end of file.
This commit is contained in:
Mikhail Golubev
2016-08-23 19:27:43 +03:00
parent 383483cc22
commit 574ddcd3a5
20 changed files with 140 additions and 60 deletions

View File

@@ -118,10 +118,11 @@ public class PythonCopyPasteProcessor implements CopyPastePreProcessor {
if (PsiTreeUtil.getParentOfType(element, PyStringLiteralExpression.class) != null) return text;
text = addLeadingSpacesToNormalizeSelection(project, text);
final String indentText = getIndentText(file, document, caretOffset, lineNumber);
final String fragmentIndent = PyIndentUtil.findCommonIndent(text, false);
final String newIndent = inferBestIndent(file, document, caretOffset, lineNumber, fragmentIndent);
final String line = document.getText(TextRange.create(lineStartOffset, lineEndOffset));
if (StringUtil.isEmptyOrSpaces(indentText) && shouldPasteOnPreviousLine(file, text, caretOffset)) {
if (StringUtil.isEmptyOrSpaces(newIndent) && shouldPasteOnPreviousLine(file, text, caretOffset)) {
caretModel.moveToOffset(lineStartOffset);
editor.getSelectionModel().setSelection(lineStartOffset, selectionModel.getSelectionEnd());
@@ -131,8 +132,8 @@ public class PythonCopyPasteProcessor implements CopyPastePreProcessor {
}
String newText;
if (StringUtil.isEmptyOrSpaces(indentText)) {
newText = PyIndentUtil.changeIndent(text, false, indentText);
if (StringUtil.isEmptyOrSpaces(newIndent)) {
newText = PyIndentUtil.changeIndent(text, false, newIndent);
}
else {
newText = text;
@@ -177,10 +178,11 @@ public class PythonCopyPasteProcessor implements CopyPastePreProcessor {
}
@NotNull
private static String getIndentText(@NotNull final PsiFile file,
@NotNull final Document document,
int caretOffset,
int lineNumber) {
private static String inferBestIndent(@NotNull PsiFile file,
@NotNull Document document,
int caretOffset,
int lineNumber,
@NotNull String fragmentIndent) {
PsiElement nonWS = PyUtil.findNextAtOffset(file, caretOffset, PsiWhiteSpace.class);
if (nonWS != null) {
@@ -196,37 +198,44 @@ public class PythonCopyPasteProcessor implements CopyPastePreProcessor {
return PyIndentUtil.getLineIndent(document, lineNumber);
}
int lineStartOffset = getLineStartSafeOffset(document, lineNumber);
final int lineStartOffset = getLineStartSafeOffset(document, lineNumber);
final PsiElement ws = file.findElementAt(lineStartOffset);
final String userIndent = document.getText(TextRange.create(lineStartOffset, caretOffset));
if (ws != null) {
PyStatementList statementList = findEmptyStatementListNearby(ws);
if (statementList != null) {
return PyIndentUtil.getElementIndent(statementList);
}
final PyStatementList statementList = findEmptyStatementListNearby(file, lineStartOffset);
if (statementList != null) {
return PyIndentUtil.getElementIndent(statementList);
}
final String smallestIndent = PyIndentUtil.getElementIndent(ws);
final PyStatementListContainer parentBlock = PsiTreeUtil.getParentOfType(ws, PyStatementListContainer.class);
final PyStatementListContainer deepestBlock = getDeepestPossibleParentBlock(ws);
final String greatestIndent;
if (deepestBlock != null && (parentBlock == null || PsiTreeUtil.isAncestor(parentBlock, deepestBlock, true))) {
greatestIndent = PyIndentUtil.getElementIndent(deepestBlock.getStatementList());
}
else {
greatestIndent = smallestIndent;
}
if (smallestIndent.startsWith(userIndent)) {
return smallestIndent;
}
if (userIndent.startsWith(greatestIndent)) {
return greatestIndent;
}
final String smallestIndent = ws == null? "" : PyIndentUtil.getElementIndent(ws);
final PyStatementListContainer parentBlock = PsiTreeUtil.getParentOfType(ws, PyStatementListContainer.class);
final PyStatementListContainer deepestBlock = getDeepestPossibleParentBlock(file, caretOffset);
final String greatestIndent;
if (deepestBlock != null && (parentBlock == null || PsiTreeUtil.isAncestor(parentBlock, deepestBlock, true))) {
greatestIndent = PyIndentUtil.getElementIndent(deepestBlock.getStatementList());
}
else {
greatestIndent = smallestIndent;
}
if (caretOffset == lineStartOffset && fragmentIndent.startsWith(smallestIndent) && greatestIndent.startsWith(fragmentIndent)) {
return fragmentIndent;
}
if (smallestIndent.startsWith(userIndent)) {
return smallestIndent;
}
if (userIndent.startsWith(greatestIndent)) {
return greatestIndent;
}
return userIndent;
}
@Nullable
private static PyStatementList findEmptyStatementListNearby(@NotNull PsiElement whitespace) {
private static PyStatementList findEmptyStatementListNearby(@NotNull PsiFile file, int offset) {
final PsiWhiteSpace whitespace = findWhitespaceAtCaret(file, offset);
if (whitespace == null) {
return null;
}
PyStatementList statementList = ObjectUtils.chooseNotNull(as(whitespace.getNextSibling(), PyStatementList.class),
as(whitespace.getPrevSibling(), PyStatementList.class));
if (statementList == null) {
@@ -239,7 +248,16 @@ public class PythonCopyPasteProcessor implements CopyPastePreProcessor {
}
@Nullable
private static PyStatementListContainer getDeepestPossibleParentBlock(@NotNull PsiElement whitespace) {
private static PsiWhiteSpace findWhitespaceAtCaret(@NotNull PsiFile file, int offset) {
return as(file.findElementAt(offset == file.getTextLength() && offset > 0 ? offset - 1 : offset), PsiWhiteSpace.class);
}
@Nullable
private static PyStatementListContainer getDeepestPossibleParentBlock(@NotNull PsiFile file, int offset) {
final PsiWhiteSpace whitespace = findWhitespaceAtCaret(file, offset);
if (whitespace == null) {
return null;
}
final PsiElement prevLeaf = getPrevNonCommentLeaf(whitespace);
return PsiTreeUtil.getParentOfType(prevLeaf, PyStatementListContainer.class);
}

View File

@@ -27,7 +27,6 @@ import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PythonFileType;
import org.jetbrains.annotations.NonNls;
@@ -215,6 +214,11 @@ public class PyIndentUtil {
return result;
}
@NotNull
public static String findCommonIndent(@NotNull String s, boolean ignoreFirstLine) {
return findCommonIndent(LineTokenizer.tokenizeIntoList(s, false, false), ignoreFirstLine);
}
/**
* Finds maximum common indentation of the given lines. Indentation of empty lines and lines containing only whitespaces is ignored unless
* they're the only lines provided. In the latter case common indentation for such lines is returned. If mix of tabs and spaces was used

View File

@@ -7,10 +7,7 @@ def foo():
bar(7)
def bar(num):
for _ in range(num):
print 'bar'
bar(7)
def bar(num):
for _ in range(num):
print 'bar'
bar(7)

View File

@@ -0,0 +1,2 @@
def f():
x = 42

View File

@@ -0,0 +1,2 @@
def f():
<caret>

View File

@@ -0,0 +1 @@
<selection>x = 42</selection>

View File

@@ -7,10 +7,8 @@ def foo():
bar(7)
def bar(num):
for _ in range(num):
print 'bar'
def bar(num):
for _ in range(num):
print 'bar'
bar(7)
bar(7)

View File

@@ -6,10 +6,7 @@ def foo():
print 'bar'
bar(7)
def bar(num):
for _ in range(num):
print 'bar'
bar(7)
def bar(num):
for _ in range(num):
print 'bar'
bar(7)

View File

@@ -0,0 +1,7 @@
def f():
def g():
a = 1
a = 1
b = 2
b = 2

View File

@@ -0,0 +1,5 @@
def f():
def g():
a = 1
<caret>
b = 2

View File

@@ -0,0 +1,3 @@
<selection>a = 1
b = 2
</selection>

View File

@@ -0,0 +1,7 @@
def foo():
a = 1
b = 2
a = 1
b = 2
x = 42

View File

@@ -0,0 +1,5 @@
def foo():
a = 1
b = 2
<caret>
x = 42

View File

@@ -0,0 +1,5 @@
def foo():
<selection> a = 1
b = 2
</selection>
x = 42

View File

@@ -0,0 +1,5 @@
def foo():
a = 1
b = 2
a = 1
b = 2

View File

@@ -0,0 +1,4 @@
def foo():
a = 1
b = 2
<caret>

View File

@@ -0,0 +1,4 @@
def foo():
<selection> a = 1
b = 2
</selection>

View File

@@ -2,9 +2,7 @@ class C:
def foo(self):
x = 1
y = 2
def foo(self):
x = 1
y = 2
def foo(self):
x = 1
y = 2

View File

@@ -3,7 +3,6 @@ class C:
x = 1
y = 2
x = 1
x = 1
def foo():
pass

View File

@@ -393,6 +393,10 @@ public class PyCopyPasteTest extends PyTestCase {
doTest();
}
public void testEmptyFunctionCaretAtEndOfFile() {
doTest();
}
// PY-19053
public void testSimpleExpressionPartCaretAtLineEnd() {
doTest();
@@ -442,4 +446,19 @@ public class PyCopyPasteTest extends PyTestCase {
public void testAsyncFunctionWithBadSelection() {
runWithLanguageLevel(LanguageLevel.PYTHON35, this::doTest);
}
// PY-20138
public void testUseExistingIndentWhenCaretAtFirstColumn() {
doTest();
}
// PY-20138
public void testUseExistingIndentWhenCaretAtFirstColumnEndOfFile() {
doTest();
}
// PY-20138
public void testInvalidExistingIndentWhenCaretAtFirstColumn() {
doTest();
}
}