Repository: spyder-ide/spyder

Issue:
Indentation errors for function calls with `#` in a string
Using Spyder 3.2.8 and reset to default settings.
Running on Xubuntu 18.04.

Situation: For an on-line Physics course, I am creating Python code that has no control statements at all. Just straight down executable statements beginning in column 1.  Call it a fancy calculator.

I find, in this situation, that Spyder automatically indents when I enter a newline after a Python print statement.  Sample code follows.
```
# Weight on Earth
m = 79 # kg
g = 9.8 # m/s^2
w = m * g # N
print("#1 w(Earth) =", w)
```
When I am positioned at the end of the print statement and press Enter, Spyder inserts a line immediately after the print line with exactly 6 spaces.  Since I had reset to default settings, the tab-indent uses 4 spaces so it doesn't seem to be a false tab.



Discussion:
I can confirm this bug.

This line produces a a tab on the next line:
```python
print("#1 w(Earth) =", w)
```

While this line doesn't:

```python
print("1 w(Earth) =", w)
```

This erroneously produces an indent of 13 spaces on the next line:
```python
 print("1 wEarth) =", w)
```
It seem the indent code doesn't recognize strings and sees the `#` in the string a comment and misses the closing parentheses of the print function.


Wow, that was an eye-opener. What a strange anomaly!  Thank you.  Omitting the '#' character inside the quoted-string of the print statement worked for me.  It never occurred to me that a '#' might be the cause.

But why exactly 6 spaces when my tab indent = 4?  Just curious.

I just coded print("#ABC") and the same thing happened.  Then, I began backspacing:
Backspace number 1 - one space removed
Backspace number 2 - one space removed
Backspace number 3 - 4 spaces removed i.e. positioned all the way left to 1st column

@texadactyl  There is 6 indentations here because if you were to write a print function that continues over multiple lines the style guide(aka PEP8) states that the indentation should go the the beginning of the function on the previous line.  In this case `print(` is 6 characters.

Like this:
```python
my_function(this='that',
            what='the like',
            other='good')
```

I wouldn't worry about "style" too much if you're just starting out and writing small scripts, but once a project becomes large and has multiple authors then it's nice to have every one on the same page.
> I wouldn't worry about "style" too much if you're just starting out and writing small scripts, but once a project becomes large and has multiple authors then it's nice to have every one on the same page.

On the other hand, its much easier to start small with a best-practice, consistent style right from the get-go, then convert a large project after the fact. Right now, in Spyder's own codebase, anything older than a few years is a real mess style-wise (maybe PEP8 didn't exist back then or something?), but even if we had the spare time to go through 10,000s or 100,000s of lines of code and comments to fix it all, it would break every existing PR and be a nightmare to merge between ``3.x`` and ``master``. 

The earlier you start with good style, the easier it is to start doing, the more you are able to benefit from it, and the more you just get used to doing things the "right" way so it builds into your muscle memory and you don't even have to think about it any more. Its the same thing with other conventions: UTF-8, LF for EOL, ISO 8601 dates, metric/SI units, etc...the earlier and more universally you implement them, the easier, more beneficial, and more natural they are.

Resolving patch:
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -2354,6 +2354,72 @@ def __number_of_spaces(self, line_text, group=0):
         if len(spaces) - 1 >= group:
             return len(spaces[group])
 
+    def __get_brackets(self, line_text, closing_brackets=[]):
+        """
+        Return unmatched opening brackets and left-over closing brackets.
+
+        (str, []) -> ([(pos, bracket)], [bracket], comment_pos)
+
+        Iterate through line_text to find unmatched brackets.
+
+        Returns three objects as a tuple:
+        1) bracket_stack:
+            a list of tuples of pos and char of each unmatched opening bracket
+        2) closing brackets:
+            this line's unmatched closing brackets + arg closing_brackets.
+            If this line ad no closing brackets, arg closing_brackets might
+            be matched with previously unmatched opening brackets in this line.
+        3) Pos at which a # comment begins. -1 if it doesn't.'
+        """
+        # Remove inline comment and check brackets
+        bracket_stack = []  # list containing this lines unmatched opening
+        # same deal, for closing though. Ignore if bracket stack not empty,
+        # since they are mismatched in that case.
+        bracket_unmatched_closing = []
+        comment_pos = -1
+        deactivate = None
+        escaped = False
+        pos, c = None, None
+        for pos, c in enumerate(line_text):
+            # Handle '\' inside strings
+            if escaped:
+                escaped = False
+            # Handle strings
+            elif deactivate:
+                if c == deactivate:
+                    deactivate = None
+                elif c == "\\":
+                    escaped = True
+            elif c in ["'", '"']:
+                deactivate = c
+            # Handle comments
+            elif c == "#":
+                comment_pos = pos
+                break
+            # Handle brackets
+            elif c in ('(', '[', '{'):
+                bracket_stack.append((pos, c))
+            elif c in (')', ']', '}'):
+                if bracket_stack and bracket_stack[-1][1] == \
+                        {')': '(', ']': '[', '}': '{'}[c]:
+                    bracket_stack.pop()
+                else:
+                    bracket_unmatched_closing.append(c)
+        del pos, deactivate, escaped
+        # If no closing brackets are left over from this line,
+        # check the ones from previous iterations' prevlines
+        if not bracket_unmatched_closing:
+            for c in list(closing_brackets):
+                if bracket_stack and bracket_stack[-1][1] == \
+                        {')': '(', ']': '[', '}': '{'}[c]:
+                    bracket_stack.pop()
+                    closing_brackets.remove(c)
+                else:
+                    break
+        del c
+        closing_brackets = bracket_unmatched_closing + closing_brackets
+        return (bracket_stack, closing_brackets, comment_pos)
+
     def fix_indent(self, *args, **kwargs):
         """Indent line according to the preferences"""
         if self.is_python_like():
@@ -2383,170 +2449,117 @@ def simple_indentation(self, forward=True, **kwargs):
     def fix_indent_smart(self, forward=True, comment_or_string=False):
         """
         Fix indentation (Python only, no text selection)
+
         forward=True: fix indent only if text is not enough indented
                       (otherwise force indent)
         forward=False: fix indent only if text is too much indented
                        (otherwise force unindent)
 
+        comment_or_string: Do not adjust indent level for
+            unmatched opening brackets and keywords
+
         Returns True if indent needed to be fixed
+
+        Assumes self.is_python_like() to return True
         """
         cursor = self.textCursor()
         block_nb = cursor.blockNumber()
         # find the line that contains our scope
-        diff_paren = 0
-        diff_brack = 0
-        diff_curly = 0
-        add_indent = False
+        line_in_block = False
+        visual_indent = False
+        add_indent = 0  # How many levels of indent to add
         prevline = None
         prevtext = ""
+
+        closing_brackets = []
         for prevline in range(block_nb-1, -1, -1):
             cursor.movePosition(QTextCursor.PreviousBlock)
             prevtext = to_text_string(cursor.block().text()).rstrip()
 
-            # Remove inline comment
-            inline_comment = prevtext.find('#')
-            if inline_comment != -1:
-                prevtext = prevtext[:inline_comment]
-
-            if ((self.is_python_like() and
-               not prevtext.strip().startswith('#') and prevtext) or
-               prevtext):
-
-                if not "return" in prevtext.strip().split()[:1] 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
-                if (prevtext.count(')') > prevtext.count('(')):
-                    diff_paren = prevtext.count(')') - prevtext.count('(')
-                elif (prevtext.count(']') > prevtext.count('[')):
-                    diff_brack = prevtext.count(']') - prevtext.count('[')
-                elif (prevtext.count('}') > prevtext.count('{')):
-                    diff_curly = prevtext.count('}') - prevtext.count('{')
-                elif diff_paren or diff_brack or diff_curly:
-                    diff_paren += prevtext.count(')') - prevtext.count('(')
-                    diff_brack += prevtext.count(']') - prevtext.count('[')
-                    diff_curly += prevtext.count('}') - prevtext.count('{')
-                    if not (diff_paren or diff_brack or diff_curly):
-                        break
+            bracket_stack, closing_brackets, comment_pos = self.__get_brackets(
+                prevtext, closing_brackets)
+            if comment_pos > -1:
+                prevtext = prevtext[:comment_pos].rstrip()
+
+            if not prevtext:
+                continue
+
+            if prevtext.endswith((':', '\\')):
+                # Presume a block was started
+                line_in_block = True  # add one level of indent to correct_indent
+                # Does this variable actually do *anything* of relevance?
+                # comment_or_string = True
+
+            if bracket_stack or not closing_brackets:
+                break
+
+        # splits of prevtext happen a few times. Let's just do it once
+        words = re.split(r'[\s\(\[\{\}\]\)]', prevtext.lstrip())
+
+        if line_in_block:
+            add_indent += 1
+
+        if prevtext and not comment_or_string:
+            if bracket_stack:
+                # Hanging indent
+                if prevtext.endswith(('(', '[', '{')):
+                    add_indent += 1
+                    if words[0] in ('class', 'def', 'elif', 'except', 'for',
+                                    'if', 'while', 'with'):
+                        add_indent += 1
+                    elif not (  # I'm not sure this block should exist here
+                            (
+                                self.tab_stop_width_spaces
+                                if self.indent_chars == '\t' else
+                                len(self.indent_chars)
+                            ) * 2 < len(prevtext)):
+                        visual_indent = True
                 else:
-                    break
+                    # There's stuff after unmatched opening brackets
+                    visual_indent = True
+            elif (words[-1] in ('continue', 'break', 'pass',)
+                  or words[0] == "return" and not line_in_block
+                  ):
+                add_indent -= 1
 
         if prevline:
-            correct_indent = self.get_block_indentation(prevline)
+            prevline_indent = self.get_block_indentation(prevline)
         else:
-            correct_indent = 0
+            prevline_indent = 0
 
-        indent = self.get_block_indentation(block_nb)
-
-        if add_indent:
+        if visual_indent:  # can only be true if bracket_stack
+            correct_indent = bracket_stack[-1][0] + 1
+        elif add_indent:
+            # Indent
             if self.indent_chars == '\t':
-                correct_indent += self.tab_stop_width_spaces
+                correct_indent = prevline_indent + self.tab_stop_width_spaces * add_indent
             else:
-                correct_indent += len(self.indent_chars)
-
-        if not comment_or_string:
-            if prevtext.endswith(':') and self.is_python_like():
-                # Indent
-                if self.indent_chars == '\t':
-                    correct_indent += self.tab_stop_width_spaces
-                else:
-                    correct_indent += len(self.indent_chars)
-            elif self.is_python_like() and \
-                (prevtext.endswith('continue') or
-                 prevtext.endswith('break') or
-                 prevtext.endswith('pass') or
-                 ("return" in prevtext.strip().split()[:1] and
-                  len(re.split(r'\(|\{|\[', prevtext)) ==
-                  len(re.split(r'\)|\}|\]', prevtext)))):
-                # Unindent
-                if self.indent_chars == '\t':
-                    correct_indent -= self.tab_stop_width_spaces
-                else:
-                    correct_indent -= len(self.indent_chars)
-            elif len(re.split(r'\(|\{|\[', prevtext)) > 1:
-
-                # Check if all braces are matching using a stack
-                stack = ['dummy']  # Dummy elemet to avoid index errors
-                deactivate = None
-                for c in prevtext:
-                    if deactivate is not None:
-                        if c == deactivate:
-                            deactivate = None
-                    elif c in ["'", '"']:
-                        deactivate = c
-                    elif c in ['(', '[','{']:
-                        stack.append(c)
-                    elif c == ')' and stack[-1] == '(':
-                        stack.pop()
-                    elif c == ']' and stack[-1] == '[':
-                        stack.pop()
-                    elif c == '}' and stack[-1] == '{':
-                        stack.pop()
-
-                if len(stack) == 1:  # all braces matching
-                    pass
+                correct_indent = prevline_indent + len(self.indent_chars) * add_indent
+        else:
+            correct_indent = prevline_indent
 
-                # Hanging indent
-                # find out if the last one is (, {, or []})
-                # only if prevtext is long that the hanging indentation
-                elif (re.search(r'[\(|\{|\[]\s*$', prevtext) is not None and
-                      ((self.indent_chars == '\t' and
-                        self.tab_stop_width_spaces * 2 < len(prevtext)) or
-                       (self.indent_chars.startswith(' ') and
-                        len(self.indent_chars) * 2 < len(prevtext)))):
-                    if self.indent_chars == '\t':
-                        correct_indent += self.tab_stop_width_spaces * 2
-                    else:
-                        correct_indent += len(self.indent_chars) * 2
-                else:
-                    rlmap = {")":"(", "]":"[", "}":"{"}
-                    for par in rlmap:
-                        i_right = prevtext.rfind(par)
-                        if i_right != -1:
-                            prevtext = prevtext[:i_right]
-                            for _i in range(len(prevtext.split(par))):
-                                i_left = prevtext.rfind(rlmap[par])
-                                if i_left != -1:
-                                    prevtext = prevtext[:i_left]
-                                else:
-                                    break
-                    else:
-                        if prevtext.strip():
-                            if len(re.split(r'\(|\{|\[', prevtext)) > 1:
-                                #correct indent only if there are still opening brackets
-                                prevexpr = re.split(r'\(|\{|\[', prevtext)[-1]
-                                correct_indent = len(prevtext)-len(prevexpr)
-                            else:
-                                correct_indent = len(prevtext)
-
-        if not (diff_paren or diff_brack or diff_curly) and \
-           not prevtext.endswith(':') and prevline:
+        # TODO untangle this block
+        if prevline and not bracket_stack 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 brackets are matched and no block gets opened
+            # Match the above line's indent and nudge to the next multiple of 4
 
-            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 cur_indent < prevline_indent and (trailing_text or is_blank):
+                # if line directly above is blank or there is text after cursor
+                # Ceiling division
+                correct_indent = -(-cur_indent // len(self.indent_chars)) * \
+                    len(self.indent_chars)
 
-        if (forward and indent >= correct_indent) or \
-           (not forward and indent <= correct_indent):
-            # No indentation fix is necessary
-            return False
+        indent = self.get_block_indentation(block_nb)
 
-        if correct_indent >= 0:
+        if correct_indent >= 0 and not (
+                indent == correct_indent or
+                forward and indent > correct_indent or
+                not forward and indent < correct_indent
+                ):
+            # Insert the determined indent
             cursor = self.textCursor()
             cursor.movePosition(QTextCursor.StartOfBlock)
             if self.indent_chars == '\t':
@@ -2560,6 +2573,7 @@ def fix_indent_smart(self, forward=True, comment_or_string=False):
                 indent_text = ' '*correct_indent
             cursor.insertText(indent_text)
             return True
+        return False
 
     @Slot()
     def clear_all_output(self):


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_autoindent.py b/spyder/plugins/editor/widgets/tests/test_autoindent.py
--- a/spyder/plugins/editor/widgets/tests/test_autoindent.py
+++ b/spyder/plugins/editor/widgets/tests/test_autoindent.py
@@ -68,19 +68,19 @@ def test_simple_def():
 def test_open_parenthesis():
     # An open parenthesis with no item is followed by a hanging indent
     text = get_indent_fix("open_parenthesis(\n")
-    assert text == "open_parenthesis(\n        ", repr(text)
+    assert text == "open_parenthesis(\n    ", repr(text)
 
 
 def test_open_bracket():
     # An open bracket with no item is followed by a hanging indent
     text = get_indent_fix("open_bracket[\n")
-    assert text == "open_bracket[\n        ", repr(text)
+    assert text == "open_bracket[\n    ", repr(text)
 
 
 def test_open_curly():
     # An open curly bracket with no item is followed by a hanging indent
     text = get_indent_fix("open_curly{\n")
-    assert text == "open_curly{\n        ", repr(text)
+    assert text == "open_curly{\n    ", repr(text)
 
 
 def test_align_on_parenthesis():
@@ -93,15 +93,15 @@ def test_align_on_parenthesis():
 def test_align_on_bracket():
     # An open bracket with one or more item is followed by an indent
     # up to the parenthesis.
-    text = get_indent_fix("bracket_w_item = (1,\n")
-    assert text == "bracket_w_item = (1,\n                  ", repr(text)
+    text = get_indent_fix("bracket_w_item = [1,\n")
+    assert text == "bracket_w_item = [1,\n                  ", repr(text)
 
 
 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)
+    text = get_indent_fix("curly_w_item = {1,\n")
+    assert text == "curly_w_item = {1,\n                ", repr(text)
 
 
 def test_keep_unindent():
@@ -182,6 +182,206 @@ def test_def_with_unindented_comment():
     assert text == "def function():\n# Comment\n    ", repr(text)
 
 
+def test_issue_5606():
+    # See spyder-ide/spyder#5606
+    text_input_expected_comment = [
+        ("if True:\n    print(str(str('foo' \n+ str('bar'))))",
+         "if True:\n    print(str(str('foo' \n                  + str('bar'))))",
+         "Before + 1"),
+        ("if True:\n    print(str(str('foo' +\n str('bar'))))",
+         "if True:\n    print(str(str('foo' +\n                  str('bar'))))",
+         "After + 1"),
+        ("if True:\n    print(str(str(str('foo') \n+ 'bar')))",
+         "if True:\n    print(str(str(str('foo') \n                  + 'bar')))",
+         "Before + 2"),
+        ("if True:\n    print(str(str(str('foo') +\n 'bar')))",
+         "if True:\n    print(str(str(str('foo') +\n                  'bar')))",
+         "After + 2"),
+        ("if True:\n    some_long_variable_name = (\n'foo' + 'bar')",
+         "if True:\n    some_long_variable_name = (\n        'foo' + 'bar')",
+         "Open paren case"),
+        ("if True:\n    (print(str(('Long string ' + 'Yeah')\n.format(some_long_variable_name))",
+         "if True:\n    (print(str(('Long string ' + 'Yeah')\n               .format(some_long_variable_name))",
+         "Before . 1"),
+        ("if True:\n    (print(str(('Long string ' + 'Yeah').\nformat(some_long_variable_name))",
+         "if True:\n    (print(str(('Long string ' + 'Yeah').\n               format(some_long_variable_name))",
+         "After . 1"),
+        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
+def test_issue_7377():
+    # See spyder-ide/spyder#7377
+    text_input_expected_comment = [
+        ("print('#1 w(Earth) =', w)\n",
+         "print('#1 w(Earth) =', w)\n",
+         "Indentation made"),
+        ("print('1 w(Earth) =', w)\n",
+         "print('1 w(Earth) =', w)\n",
+         "Indentation made"),
+        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
+def test_issue_887():
+    # See spyder-ide/spyder#887
+    text_input_expected_comment = [
+        ("df = pd.read_csv(fpath,\n" +
+         "                 # HINT indexing begins with 0\n" +
+         "                 header=0, # headers 1st row\n" +
+         "                 index_col=0, #timestamps 1st column\n" +
+         "                 # specify as seconds to recognize as epoch timestamp...\n" +
+         "                 # it's local time (i.e...)\n" +
+         "                 # (section 9.10.3, http://......)\n" +
+         "                 date_parser=lambda x: pd.to_datetime(x, units='s'),\n" +
+         "            parse_dates=True)",
+         "df = pd.read_csv(fpath,\n" +
+         "                 # HINT indexing begins with 0\n" +
+         "                 header=0, # headers 1st row\n" +
+         "                 index_col=0, #timestamps 1st column\n" +
+         "                 # specify as seconds to recognize as epoch timestamp...\n" +
+         "                 # it's local time (i.e...)\n" +
+         "                 # (section 9.10.3, http://......)\n" +
+         "                 date_parser=lambda x: pd.to_datetime(x, units='s'),\n" +
+         "                 parse_dates=True)",
+         "Indentation made"),
+        ("df = pd.read_csv(fpath,\n" +
+         "                 # HINT indexing begins with 0\n" +
+         "                 header=0, # headers 1st row\n" +
+         "                 index_col=0, #timestamps 1st column\n" +
+         "                 # specify as seconds to recognize as epoch timestamp...\n" +
+         "                 # it's local time (i.e...)\n" +
+         "                 # (section 9.10.3, http://......)\n" +
+         "                 #date_parser=lambda x: pd.to_datetime(x, units='s'),\n" +
+         "            parse_dates=True)",
+         "df = pd.read_csv(fpath,\n" +
+         "                 # HINT indexing begins with 0\n" +
+         "                 header=0, # headers 1st row\n" +
+         "                 index_col=0, #timestamps 1st column\n" +
+         "                 # specify as seconds to recognize as epoch timestamp...\n" +
+         "                 # it's local time (i.e...)\n" +
+         "                 # (section 9.10.3, http://......)\n" +
+         "                 #date_parser=lambda x: pd.to_datetime(x, units='s'),\n" +
+         "                 parse_dates=True)",
+         "Indentation made"),        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
+def test_keywords():
+    # 'def', 'for', 'if', 'while', 'with', 'class', 'elif', 'except'
+    text_input_expected_comment = [
+        ("def func():\n", "def func():\n    ",
+         "function declaration"),
+        ("def function(\n", "def function(\n        ",
+         "partial function declaration"),
+        ("def f(\n", "def f(\n        ",
+         "partial function declaration with short name"),
+        ("def func(): return\n", "def func(): return\n", "single line return"),
+        ("def func():\n    if True:\n        return\n",
+         "def func():\n    if True:\n        return\n    ",
+         "return in if block"),
+        ("if True:\n    varname = multipass\n",
+         "if True:\n    varname = multipass\n    ",
+         "endswith pass, but not keyword"),
+        ("def func():\n    return()\n", "def func():\n    return()\n",
+         "no space between return and bracket"),
+        ("for i in range(\n", "for i in range(\n        ", "partial for"),
+        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
+def test_hanging_indentation_without_block():
+    text_input_expected_comment = [
+        ("function(\n",
+         "function(\n    ",
+         "function call"),
+        ("f(\n",
+         "f(\n  ",
+         "function call with short name"),
+        ("def f():\n    return \\\n", "def f():\n    return \\\n        ",
+         "line cont. return"),
+        ("def f():\n    return lambda: \\\n",
+         "def f():\n    return lambda: \\\n        ",
+         "return but indent")
+        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
+def test_no_hanging_indentation():
+    text_input_expected_comment = [
+        ("f('(')\n",
+         "f('(')\n",
+         "open bracket in string"),
+        ("f('def f(')",
+         "f('def f(')",
+         "keyword in string"),
+        ("'a(a,'\n",
+         "'a(a,'\n",
+         "bracket in string"),
+        ("def f():\n    return()\n",
+         "def f():\n    return()\n",
+         "return without space after"),
+        ("print('#1 w(Earth) =', w)\n",
+         "print('#1 w(Earth) =', w)\n",
+         "# in string")
+        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
+def test_unintent_in_block():
+    # return, yield, pass, continue, break, raise
+    text = get_indent_fix("def f(x):\n    if True:\n        return\n")
+    assert text == "def f(x):\n    if True:\n        return\n    ", repr(text)
+
+
+def test_multi_line_lists():
+#         "corbin_dallas = multipass"
+# if True: pass
+    # if (
+    #     pass
+    # if True: if True: (invalid syntax)
+    # if True: return
+    # def f():\n# comment\n#
+    text_input_expected_comment = [
+        ('[\n    ("",\n     "",\n     "")\n',
+         '[\n    ("",\n     "",\n     "")\n    ',
+         "multiline args"),
+        ('[\n    ("",\n     "",\n     ""),\n',
+         '[\n    ("",\n     "",\n     ""),\n    ',
+         "multiline args with comma")
+        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
+def test_string_literals():
+    text_input_expected_comment = [
+        ("'''\n", "'''\n", "bare opening triple-quote"),
+        ("def f():\n    '''\n", "def f():\n    '''\n    ",
+         "opening triple-quote in function declaration"),
+        ("'''Docstring'''\n", "'''Docstring'''\n",
+         "triple-quoted string literal"),
+        ("", "", ""),
+        ("", "", ""),
+        ]
+    for text_input, expected, comment in text_input_expected_comment:
+        text = get_indent_fix(text_input)
+        assert text == expected, comment
+
+
 # ---- Tabs tests
 def test_indentation_with_tabs():
     text_input_expected_comment = [
@@ -191,10 +391,10 @@ def test_indentation_with_tabs():
          "test with indented comment"),
         ("def function():\n\tprint []\n", "def function():\n\tprint []\n\t",
          "test brackets alone"),
-        ("\nsome_long_name = {\n", "\nsome_long_name = {\n\t\t",
+        ("\nsome_long_name = {\n", "\nsome_long_name = {\n\t",
          "indentation after opening bracket"),
         ("def function():\n", "def function():\n\t", "test simple def"),
-        ("open_parenthesis(\n", "open_parenthesis(\n\t\t",
+        ("open_parenthesis(\n", "open_parenthesis(\n\t",
          "open parenthesis"),
         ("tags = ['(a)', '(b)', '(c)']\n", "tags = ['(a)', '(b)', '(c)']\n",
          "test_commented_brackets"),
@@ -220,8 +420,8 @@ def test_indentation_with_tabs():
 def test_indentation_with_tabs_parenthesis():
     """Simple parenthesis indentation test with different tab stop widths."""
     text_input_expected_tab = [
-        ("print(\n)", "print(\n\t\t)", 1),
-        ("print(\n)", "print(\n\t\t)", 2),
+        ("print(\n)", "print(\n\t)", 1),
+        ("print(\n)", "print(\n\t)", 2),
         ("print(\n)", "print(\n\t\t)", 3),
         ("print(\n)", "print(\n\t  )", 4),
         ("print(\n)", "print(\n\t )", 5),