Repository: spyder-ide/spyder

Issue:
Auto indentation of line following indented block
The Problem:
If I have a block of indented code, followed by a block of un-indented code, and I attempt to insert additional (blank) lines between the two, pressing enter causes the un-indented line of code to automatically indent. 

If this is a bug, it needs to be fixed, if it is a "feature" then it needs to be made optional. 

**What steps will reproduce the problem?**
1. A line of indented code, followed by a line of un-indented code.
2. Press enter on the un-indented line of code.
![image](https://cloud.githubusercontent.com/assets/20540949/20987775/2fbd1654-bc8b-11e6-89dd-e00c88392ada.png)

**What is the expected output? What do you see instead?**
Expected Output: the "un-indented" line of code moves down a line, and remains un-indented.
![image](https://cloud.githubusercontent.com/assets/20540949/20987824/59b5c3d4-bc8b-11e6-8263-26ca46f509be.png)

Actual Output: the "un-indented" line of code moves down a line and becomes indented.
![image](https://cloud.githubusercontent.com/assets/20540949/20987803/4708f5f8-bc8b-11e6-99e7-0b933abb57f3.png)

**Please provide any additional information below**
Note, this occurs if the two lines directly follow each other, or if there are additional blank (un-indented) lines between them. 

I tried toggling the auto indention in the options, but it didn't make a difference.
![image](https://cloud.githubusercontent.com/assets/20540949/20988220/07f4a478-bc8d-11e6-9eab-8dadf7d819cf.png)
 
Thanks.

## Versions and main components
* Spyder Version: 3.0.2
* Python Version: 3.5.2 64bits, Qt 5.6.0, PyQt5 5.6 on Windows
* Operating system: Windows 10


## Dependencies
jedi >=0.8.1     :  0.9.0 (OK)
matplotlib >=1.0 :  1.5.3 (OK)
nbconvert >=4.0  :  4.2.0 (OK)
numpy >=1.7      :  1.11.1 (OK)
pandas >=0.13.1  :  0.18.1 (OK)
pep8 >=0.6       :  1.7.0 (OK)
pyflakes >=0.6.0 :  1.3.0 (OK)
pygments >=2.0   :  2.1.3 (OK)
pylint >=0.25    :  1.6.4 (OK)
qtconsole >=4.2.0:  4.2.1 (OK)
rope >=0.9.4     :  0.9.4-1 (OK)
sphinx >=0.6.6   :  1.5 (OK)
sympy >=0.7.3    :  1.0 (OK)



Discussion:

Resolving patch:
diff --git a/spyder/widgets/sourcecode/codeeditor.py b/spyder/widgets/sourcecode/codeeditor.py
--- a/spyder/widgets/sourcecode/codeeditor.py
+++ b/spyder/widgets/sourcecode/codeeditor.py
@@ -1920,18 +1920,18 @@ def fix_indent(self, forward=True, comment_or_string=False):
         for prevline in range(block_nb-1, -1, -1):
             cursor.movePosition(QTextCursor.PreviousBlock)
             prevtext = to_text_string(cursor.block().text()).rstrip()
-            
-            if ((self.is_python_like() 
-                 and not prevtext.strip().startswith('#')
-                 and prevtext) 
-                or prevtext):
-                    
-                if (prevtext.strip().endswith(')')
-                    or prevtext.strip().endswith(']')
-                    or prevtext.strip().endswith('}')):
-                
+
+            if ((self.is_python_like() and
+               not prevtext.strip().startswith('#') and prevtext) or
+               prevtext):
+
+                if not prevtext.strip().startswith('return') and \
+                    (prevtext.strip().endswith(')') or
+                     prevtext.strip().endswith(']') or
+                     prevtext.strip().endswith('}')):
+
                     comment_or_string = True  # prevent further parsing
-                    
+
                 elif prevtext.strip().endswith(':') and self.is_python_like():
                     add_indent = True
                     comment_or_string = True
@@ -1970,8 +1970,13 @@ def fix_indent(self, forward=True, comment_or_string=False):
                     correct_indent += self.tab_stop_width_spaces
                 else:
                     correct_indent += len(self.indent_chars)
-            elif (prevtext.endswith('continue') or prevtext.endswith('break') \
-              or prevtext.endswith('pass')) and self.is_python_like():
+            elif self.is_python_like() and \
+                (prevtext.endswith('continue') or
+                 prevtext.endswith('break') or
+                 prevtext.endswith('pass') or
+                 (prevtext.strip().startswith("return") and
+                  len(re.split(r'\(|\{|\[', prevtext)) ==
+                  len(re.split(r'\)|\}|\]', prevtext)))):
                 # Unindent
                 if self.indent_chars == '\t':
                     correct_indent -= self.tab_stop_width_spaces
@@ -2006,6 +2011,22 @@ def fix_indent(self, forward=True, comment_or_string=False):
                             else:
                                 correct_indent = len(prevtext)
 
+        if not (diff_paren or diff_brack or diff_curly) and \
+           not prevtext.endswith(':'):
+            cur_indent = self.get_block_indentation(block_nb - 1)
+            is_blank = not self.get_text_line(block_nb - 1).strip()
+            prevline_indent = self.get_block_indentation(prevline)
+            trailing_text = self.get_text_line(block_nb).strip()
+
+            if cur_indent < prevline_indent and \
+               (trailing_text or is_blank):
+                if cur_indent % len(self.indent_chars) == 0:
+                    correct_indent = cur_indent
+                else:
+                    correct_indent = cur_indent \
+                                   + (len(self.indent_chars) -
+                                      cur_indent % len(self.indent_chars))
+
         if (forward and indent >= correct_indent) or \
            (not forward and indent <= correct_indent):
             # No indentation fix is necessary


Test patch:
diff --git a/spyder/widgets/sourcecode/tests/test_autoindent.py b/spyder/widgets/sourcecode/tests/test_autoindent.py
--- a/spyder/widgets/sourcecode/tests/test_autoindent.py
+++ b/spyder/widgets/sourcecode/tests/test_autoindent.py
@@ -20,7 +20,9 @@
 
 # --- Fixtures
 # -----------------------------------------------------------------------------
-def get_indent_fix(text, indent_chars=" " * 4, tab_stop_width_spaces=4):
+def get_indent_fix(text, indent_chars=" " * 4, tab_stop_width_spaces=4,
+                   sol=False):
+    """Return text with last line's indentation fixed."""
     app = qapplication()
     editor = CodeEditor(parent=None)
     editor.setup_editor(language='Python', indent_chars=indent_chars,
@@ -29,6 +31,10 @@ def get_indent_fix(text, indent_chars=" " * 4, tab_stop_width_spaces=4):
     editor.set_text(text)
     cursor = editor.textCursor()
     cursor.movePosition(QTextCursor.End)
+    if sol:
+        lines = text.splitlines(True)
+        repeat = len(lines[-1].lstrip())
+        cursor.movePosition(QTextCursor.Left, n=repeat)
     editor.setTextCursor(cursor)
     editor.fix_indent()
     return to_text_string(editor.toPlainText())
@@ -92,7 +98,47 @@ def test_align_on_curly():
     # An open curly bracket with one or more item is followed by an indent
     # up to the parenthesis.
     text = get_indent_fix("curly_w_item = (1,\n")
-    assert text == "curly_w_item = (1,\n                ", repr(text)    
+    assert text == "curly_w_item = (1,\n                ", repr(text)
+
+
+def test_keep_unindent():
+    # Keep line unindented if there is more than one line under the statement
+    text = ("    def foo(bar):\n"
+            "        generic = bar\n"
+            "    \n"
+            "    keep_unindent\n")
+    correct_text = ("    def foo(bar):\n"
+                    "        generic = bar\n"
+                    "    \n"
+                    "    keep_unindent\n")
+    text = get_indent_fix(text, sol=True)
+    assert text == correct_text, repr(text)
+
+
+def test_keep_unindent_fix_indent():
+    # Keep line unindented but fix indent if not multiple of len(indent_chars)
+    text = ("    for x in range(n):\n"
+            "        increment += 1\n"
+            "  \n"
+            "  keep_unindent\n")
+    correct_text = ("    for x in range(n):\n"
+                    "        increment += 1\n"
+                    "  \n"
+                    "    keep_unindent\n")
+    text = get_indent_fix(text, sol=True)
+    assert text == correct_text, repr(text)
+
+
+def test_keep_unindent_if_blank():
+    # Keep line unindented if return is pressed on a line which is both
+    # blank and unindented.
+    text = ("    def f(x):\n"
+            "        return x\n"
+            "\n"
+            "")
+    text = get_indent_fix(text)
+    assert text == "    def f(x):\n        return x\n\n", repr(text)
+
 
 # --- Failing tests
 # -----------------------------------------------------------------------------