PY-20138 Use existing indent of pasted fragment if caret is at first column
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Tue, 23 Aug 2016 16:27:43 +0000 (19:27 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Tue, 11 Oct 2016 14:24:06 +0000 (17:24 +0300)
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.

20 files changed:
python/src/com/jetbrains/python/editor/PythonCopyPasteProcessor.java
python/src/com/jetbrains/python/psi/PyIndentUtil.java
python/testData/copyPaste/CaretAtTheBeginningOfIndent.after.py
python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.after.py [new file with mode: 0644]
python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.dst.py [new file with mode: 0644]
python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.src.py [new file with mode: 0644]
python/testData/copyPaste/EmptyLineInList.after.py
python/testData/copyPaste/InnerToOuterFunction.after.py
python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.after.py [new file with mode: 0644]
python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.dst.py [new file with mode: 0644]
python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.src.py [new file with mode: 0644]
python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.after.py [new file with mode: 0644]
python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.dst.py [new file with mode: 0644]
python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.src.py [new file with mode: 0644]
python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.after.py [new file with mode: 0644]
python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.dst.py [new file with mode: 0644]
python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.src.py [new file with mode: 0644]
python/testData/copyPaste/multiLine/DecreaseIndent.after.py
python/testData/copyPaste/singleLine/IndentOnTopLevel.after.py
python/testSrc/com/jetbrains/python/PyCopyPasteTest.java

index bcc71b474ec9aabf91b9b6bdf3b6cde1014479b4..c78bef15ab32d8671aafbdc590671ec4aae1d066 100644 (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);
   }
index 3615997a888bc38ec23b0e5870c265e5618d24a7..060246b66c77936a5b5cac435ee3e2ad56b3d3a1 100644 (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
index 9f86fdf7e2f6e74ccec2f63efee7832cc11846dd..402353103c27017a734171e7c58d662aa4a5c4ac 100644 (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)
diff --git a/python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.after.py b/python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.after.py
new file mode 100644 (file)
index 0000000..e5b99d1
--- /dev/null
@@ -0,0 +1,2 @@
+def f():
+    x = 42
\ No newline at end of file
diff --git a/python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.dst.py b/python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.dst.py
new file mode 100644 (file)
index 0000000..e1b13dd
--- /dev/null
@@ -0,0 +1,2 @@
+def f():
+<caret>
\ No newline at end of file
diff --git a/python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.src.py b/python/testData/copyPaste/EmptyFunctionCaretAtEndOfFile.src.py
new file mode 100644 (file)
index 0000000..69bdd77
--- /dev/null
@@ -0,0 +1 @@
+<selection>x = 42</selection>
\ No newline at end of file
index 9f86fdf7e2f6e74ccec2f63efee7832cc11846dd..8ce901b10347e98c2cadab88864aefa612e45cc2 100644 (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)
index a47bcea0e8e3fbe3ceb0f401a7bb832f3d0dd4aa..f5f9835c675963f8deae9fcd7fecd773a80645af 100644 (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)
diff --git a/python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.after.py b/python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.after.py
new file mode 100644 (file)
index 0000000..0957695
--- /dev/null
@@ -0,0 +1,7 @@
+def f():
+    def g():
+        a = 1
+        a = 1
+        b = 2
+
+        b = 2
diff --git a/python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.dst.py b/python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.dst.py
new file mode 100644 (file)
index 0000000..c67ef0d
--- /dev/null
@@ -0,0 +1,5 @@
+def f():
+    def g():
+        a = 1
+<caret>
+        b = 2
diff --git a/python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.src.py b/python/testData/copyPaste/InvalidExistingIndentWhenCaretAtFirstColumn.src.py
new file mode 100644 (file)
index 0000000..201d205
--- /dev/null
@@ -0,0 +1,3 @@
+<selection>a = 1
+b = 2
+</selection>
\ No newline at end of file
diff --git a/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.after.py b/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.after.py
new file mode 100644 (file)
index 0000000..e74c6d8
--- /dev/null
@@ -0,0 +1,7 @@
+def foo():
+    a = 1
+    b = 2
+    a = 1
+    b = 2
+
+x = 42
\ No newline at end of file
diff --git a/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.dst.py b/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.dst.py
new file mode 100644 (file)
index 0000000..49b03d1
--- /dev/null
@@ -0,0 +1,5 @@
+def foo():
+    a = 1
+    b = 2
+<caret>
+x = 42
\ No newline at end of file
diff --git a/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.src.py b/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumn.src.py
new file mode 100644 (file)
index 0000000..0369052
--- /dev/null
@@ -0,0 +1,5 @@
+def foo():
+<selection>    a = 1
+    b = 2
+</selection>
+x = 42
\ No newline at end of file
diff --git a/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.after.py b/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.after.py
new file mode 100644 (file)
index 0000000..625881d
--- /dev/null
@@ -0,0 +1,5 @@
+def foo():
+    a = 1
+    b = 2
+    a = 1
+    b = 2
diff --git a/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.dst.py b/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.dst.py
new file mode 100644 (file)
index 0000000..c0a35e3
--- /dev/null
@@ -0,0 +1,4 @@
+def foo():
+    a = 1
+    b = 2
+<caret>
\ No newline at end of file
diff --git a/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.src.py b/python/testData/copyPaste/UseExistingIndentWhenCaretAtFirstColumnEndOfFile.src.py
new file mode 100644 (file)
index 0000000..678d014
--- /dev/null
@@ -0,0 +1,4 @@
+def foo():
+<selection>    a = 1
+    b = 2
+</selection>
\ No newline at end of file
index a7065ada7fdca040ac5cae6a7849c27c0d15d691..0631c38d4e6d2c5b5be5e2de22cbc06e01dd8e81 100644 (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
 
index 50ab099f253c0a81e67ba03d8da16195005f410c..af216978d573f43d88daaa0de1e35727e1c0374f 100644 (file)
@@ -3,7 +3,6 @@ class C:
         x = 1
         y = 2
 
-
-x = 1
+        x = 1
 def foo():
     pass
\ No newline at end of file
index 63f3ef6de40fb8047cea73570ade7b9b5f703525..95fe4b94b6b4f53ad21a333a6b37308d1221ba5f 100644 (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();
+  }
 }