Repository: spyder-ide/spyder

Issue:
Indentation issue using Tabs
## Description of your problem

Using tabs for indentation, when indenting a new line in the middle of a statement (e.g. between parentheses/braces/brackets), it inserts more tabs than expected (looks like current indentation + 5), and there is no way to configure it.  
**What steps will reproduce the problem?**
1. Set up the editor to use Tabulations for indentation
2. in the editor, type a = {}
3. with the cursor between the braces hit enter

**What is the expected output? What do you see instead?**
expected: the new line is indented with 1 (or 2; configurable) tabs
actual: the new line is indented with 5 tabs

**Please provide any additional information below**
Hitting tab after removing any/all indentation immediately adds back to the original number of tabs.
Even if the first line in the continuation block (additional lines associated with a single statement) is indented the way I expect, subsequent lines still start with the original number of tabs rather than aligning with the previous line.  This is the most obnoxious part of this issue as that means I have to manually correct each line of a continuation block.

Suggested behavior:
Continuation lines should indent with one more unit of indentation than the parent of that continuation block.  Subsequent lines should indent with the same indentation as the previous line.  Continuation blocks may be nested (e.g. if calling a function with many parameters, where one parameter is another function call with many parameters and I want to have each parameter on its own line, or if defining a nested json-like structure of dicts and lists), in which case each new nesting would increase the indentation by one unit of indentation.

It would be nice if the continuation indentation were configurable as well, as some users may prefer 2 indents instead of 1.  
## Versions and main components
- Spyder Version: 3.0.0
- Python Version: 3.4.3
- Operating system: Ubuntu 14.04
- Qt and PyQt Version: 5.2.1
## Dependencies

Please go to the menu entry `Help > Optional Dependencies` (or
`Help > Dependencies`), press the button `Copy to clipboard`
and paste the contents below:

I don't see a menu.  
I installed it using pip.



Discussion:
Wait, I lied a bit.  Subsequent lines in a continuation do behave as expected, carrying over the previous line's indentation.   I think I may have been triggering the new continuation line multiple times over (like where it tries to be smart about where a new line should start after several blank lines and/or comment-only lines)

The original case in the steps to reproduce is still a problem (tested again with spyder 3.0.1).  My previous comment was only that subsequent lines behave as expected.

@rlaverde, please take a look at this one.

Additional information in some testing just now:
If the opening brace/bracket/paren is on its own line and a new line is added after it, the new line adds 1 more tab as expected.  If there is other stuff before the brace/bracket/paren then it adds the extra tabs (looks like one for each non-indentation character before the bracket).  

I suppose there could be some who prefer that behavior with spaces to help align the next lines with the opening brace, but with tabs that just doesn't work very well.  


Resolving patch:
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -382,6 +382,7 @@ def get_word_at(self, coordinates):
     def get_block_indentation(self, block_nb):
         """Return line indentation (character number)"""
         text = to_text_string(self.document().findBlockByNumber(block_nb).text())
+        text = text.replace("\t", " "*4)
         return len(text)-len(text.lstrip())
     
     def get_selection_bounds(self):
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
@@ -1926,23 +1926,33 @@ def fix_indent(self, forward=True, comment_or_string=False):
                 else:
                     break
 
-        if not prevline:
-            return False
+        if prevline:
+            correct_indent = self.get_block_indentation(prevline)
+        else:
+            correct_indent = 0
 
         indent = self.get_block_indentation(block_nb)
-        correct_indent = self.get_block_indentation(prevline)
 
         if add_indent:
-            correct_indent += len(self.indent_chars)
+            if self.indent_chars == '\t':
+                correct_indent += 4
+            else:
+                correct_indent += len(self.indent_chars)
 
         if not comment_or_string:
             if prevtext.endswith(':') and self.is_python_like():
                 # Indent
-                correct_indent += len(self.indent_chars)
+                if self.indent_chars == '\t':
+                    correct_indent += 4
+                else:
+                    correct_indent += len(self.indent_chars)
             elif (prevtext.endswith('continue') or prevtext.endswith('break') \
               or prevtext.endswith('pass')) and self.is_python_like():
                 # Unindent
-                correct_indent -= len(self.indent_chars)
+                if self.indent_chars == '\t':
+                    correct_indent -= 4
+                else:
+                    correct_indent -= len(self.indent_chars)
             elif len(re.split(r'\(|\{|\[', prevtext)) > 1:
                 rlmap = {")":"(", "]":"[", "}":"{"}
                 for par in rlmap:
@@ -1974,7 +1984,12 @@ def fix_indent(self, forward=True, comment_or_string=False):
             cursor.movePosition(QTextCursor.StartOfBlock)
             cursor.setPosition(cursor.position()+indent, QTextCursor.KeepAnchor)
             cursor.removeSelectedText()
-            cursor.insertText(self.indent_chars[0]*correct_indent)
+            if self.indent_chars == '\t':
+                indent_text = '\t' * (correct_indent // 4) \
+                            + ' ' * (correct_indent % 4)
+            else:
+                indent_text = ' '*correct_indent
+            cursor.insertText(indent_text)
             return True
 
     @Slot()


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,10 +20,10 @@
 
 # --- Fixtures
 # -----------------------------------------------------------------------------
-def get_indent_fix(text):
+def get_indent_fix(text, indent_chars=" " * 4):
     app = qapplication()
     editor = CodeEditor(parent=None)
-    editor.setup_editor(language='Python')
+    editor.setup_editor(language='Python', indent_chars=indent_chars)
 
     editor.set_text(text)
     cursor = editor.textCursor()
@@ -49,28 +49,54 @@ def test_def_with_indented_comment():
     text = get_indent_fix("def function():\n    # Comment\n")
     assert text == "def function():\n    # Comment\n    ", repr(text)
 
+
 def test_brackets_alone():
     text = get_indent_fix("def function():\n    print []\n")
     assert text == "def function():\n    print []\n    ", repr(text)
 
-# --- Failing tests
-# -----------------------------------------------------------------------------
-@pytest.mark.xfail
+
+def test_open_parenthesis():
+    text = get_indent_fix("open_parenthesis(\n")
+    assert text == "open_parenthesis(\n                 ", repr(text)
+
+
 def test_simple_def():
     text = get_indent_fix("def function():\n")
     assert text == "def function():\n    ", repr(text)
 
 
+# --- Failing tests
+# -----------------------------------------------------------------------------
 @pytest.mark.xfail
 def test_def_with_unindented_comment():
     text = get_indent_fix("def function():\n# Comment\n")
     assert text == "def function():\n# Comment\n    ", repr(text)
 
 
-@pytest.mark.xfail
-def test_open_parenthesis():
-    text = get_indent_fix("open_parenthesis(\n")
-    assert text == "open_parenthesis(\n    ", repr(text)
+# --- Tabs tests
+# -----------------------------------------------------------------------------
+@pytest.mark.parametrize(
+    "text_input, expected, test_text",
+    [
+        ("this_tuple = (1, 2)\n", "this_tuple = (1, 2)\n", "simple tuple"),
+        ("\ndef function():\n", "\ndef function():\n\t", "def with new line"),
+        ("def function():\n\t# Comment\n", "def function():\n\t# Comment\n\t",
+         "test with indented comment"),
+        ("def function():\n\tprint []\n", "def function():\n\tprint []\n\t",
+         "test brackets alone"),
+        ("\na = {\n", "\na = {\n\t ", "indentation after opening bracket"),
+        ("def function():\n", "def function():\n\t", "test simple def"),
+
+        # Failing test
+        pytest.mark.xfail(
+            ("def function():\n# Comment\n", "def function():\n# Comment\n\t",
+             "test_def_with_unindented_comment")),
+        pytest.mark.xfail(("open_parenthesis(\n", "open_parenthesis(\n\t\t\t\ŧ ",
+                           "open parenthesis")),
+    ])
+def test_indentation_with_tabs(text_input, expected, test_text):
+    text = get_indent_fix(text_input, indent_chars="\t")
+    assert text == expected, test_text
 
 
 if __name__ == "__main__":