Repository: spyder-ide/spyder

Issue:
Code folding does not fold correctly when there are blank lines
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

copy this dict in the editor:

```python
responses = {
    100: ('Continue', 'Request received, please continue'),
    101: ('Switching Protocols','Switching to new protocol; obey Upgrade header'),
    200: ('OK', 'Request fulfilled, document follows'),
    201: ('Created', 'Document created, URL follows'),
    202: ('Accepted','Request accepted, processing continues off-line'),
    203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
    204: ('No Content', 'Request fulfilled, nothing follows'),
    205: ('Reset Content', 'Clear input form for further input.'),
    206: ('Partial Content', 'Partial content follows.'),
    300: ('Multiple Choices','Object has several resources -- see URI list'),
    301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
    302: ('Found', 'Object moved temporarily -- see URI list'),
    303: ('See Other', 'Object moved -- see Method and URL list'),
    304: ('Not Modified',
          'Document has not changed since given time'),
    305: ('Use Proxy',
          'You must use proxy specified in Location to access this '
          'resource.'),
    307: ('Temporary Redirect',
          'Object moved temporarily -- see URI list'),

    400: ('Bad Request',
          'Bad request syntax or unsupported method'),
    401: ('Unauthorized',
          'No permission -- see authorization schemes'),
    402: ('Payment Required',
          'No payment -- see charging schemes'),
    403: ('Forbidden',
          'Request forbidden -- authorization will not help'),
    404: ('Not Found', 'Nothing matches the given URI'),
    405: ('Method Not Allowed',
          'Specified method is invalid for this server.'),
    406: ('Not Acceptable', 'URI not available in preferred format.'),
    407: ('Proxy Authentication Required', 'You must authenticate with '
          'this proxy before proceeding.'),
    408: ('Request Timeout', 'Request timed out; try again later.'),
    409: ('Conflict', 'Request conflict.'),
    410: ('Gone',
          'URI no longer exists and has been permanently removed.'),
    411: ('Length Required', 'Client must specify Content-Length.'),
    412: ('Precondition Failed', 'Precondition in headers is false.'),
    413: ('Request Entity Too Large', 'Entity is too large.'),
    414: ('Request-URI Too Long', 'URI is too long.'),
    415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
    416: ('Requested Range Not Satisfiable',
          'Cannot satisfy request range.'),
    417: ('Expectation Failed',
          'Expect condition could not be satisfied.'),

    500: ('Internal Server Error', 'Server got itself in trouble'),
    501: ('Not Implemented',
          'Server does not support this operation'),
    502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
    503: ('Service Unavailable',
          'The server cannot process the request due to a high load'),
    504: ('Gateway Timeout',
          'The gateway server did not receive a timely response'),
    505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
}
```

the code folding will not fold the entire dict because of the newlines. instead it will fold the code at every new line. i dont think this behaviour is intended



## Versions

* Spyder version: 4.0.0b2 
* Python version: 3.7.3
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 10

### Dependencies

```
pygments >=2.0     :  2.4.0 (OK)
qdarkstyle >=2.6.4 :  2.6.8 (OK)
sphinx >=0.6.6     :  2.0.1 (OK)
pyls >=0.19.0;<0.25:  0.24.0 (OK)
nbconvert >=4.0    :  5.5.0 (OK)
pandas >=0.13.1    :  0.24.2 (OK)
numpy >=1.7        :  1.16.3 (OK)
sympy >=0.7.3      :  None (NOK)
cython >=0.21      :  None (NOK)
qtconsole >=4.2.0  :  4.4.4 (OK)
IPython >=4.0      :  7.5.0 (OK)
matplotlib >=2.0.0 :  3.0.3 (OK)
pylint >=0.25      :  2.3.1 (OK)
```



Discussion:
Thanks for reporting. We'll take a look at it in beta3.
@steff456, please take a look at this one.

Resolving patch:
diff --git a/spyder/plugins/editor/api/folding.py b/spyder/plugins/editor/api/folding.py
--- a/spyder/plugins/editor/api/folding.py
+++ b/spyder/plugins/editor/api/folding.py
@@ -39,29 +39,20 @@ def print_tree(editor, file=sys.stdout, print_blocks=False, return_list=False):
 
     block = editor.document().firstBlock()
     while block.isValid():
-        trigger = TextBlockHelper().is_fold_trigger(block)
-        trigger_state = TextBlockHelper().is_collapsed(block)
         lvl = TextBlockHelper().get_fold_lvl(block)
         visible = 'V' if block.isVisible() else 'I'
-        if trigger:
-            trigger = '+' if trigger_state else '-'
-            if return_list:
-                output_list.append([block.blockNumber() + 1, lvl, visible])
-            else:
-                print('l%d:%s%s%s' %
-                      (block.blockNumber() + 1, lvl, trigger, visible),
-                      file=file)
-        elif print_blocks:
-            if return_list:
-                output_list.append([block.blockNumber() + 1, lvl, visible])
-            else:
-                print('l%d:%s%s' %
-                      (block.blockNumber() + 1, lvl, visible), file=file)
+        if return_list:
+            output_list.append([block.blockNumber() + 1, lvl, visible])
+        else:
+            print('l%d:%s%s' %
+                  (block.blockNumber() + 1, lvl, visible),
+                  file=file)
         block = block.next()
 
     if return_list:
         return output_list
 
+
 class FoldDetector(object):
     """
     Base class for fold detectors.
@@ -112,7 +103,6 @@ def process_block(self, current_block, previous_block, text):
                 previous_block, current_block)
             if fold_level > self.limit:
                 fold_level = self.limit
-
         prev_fold_level = TextBlockHelper.get_fold_lvl(previous_block)
 
         if fold_level > prev_fold_level:
@@ -124,13 +114,11 @@ def process_block(self, current_block, previous_block, text):
                 block = block.previous()
             TextBlockHelper.set_fold_trigger(
                 block, True)
-
         # update block fold level
         if text.strip() and not self.editor.is_comment(previous_block):
             TextBlockHelper.set_fold_trigger(
                 previous_block, fold_level > prev_fold_level)
         TextBlockHelper.set_fold_lvl(current_block, fold_level)
-
         # user pressed enter at the beginning of a fold trigger line
         # the previous blank or comment line will keep the trigger state
         # and the new line (which actually contains the trigger) must use
diff --git a/spyder/plugins/editor/utils/folding.py b/spyder/plugins/editor/utils/folding.py
--- a/spyder/plugins/editor/utils/folding.py
+++ b/spyder/plugins/editor/utils/folding.py
@@ -18,6 +18,7 @@
 
 # Standard library imports
 import sys
+import re
 
 # Local imports
 from spyder.plugins.editor.api.folding import FoldDetector
@@ -222,6 +223,8 @@ def detect_fold_level(self, prev_block, block):
         """
         text = block.text()
         prev_lvl = TextBlockHelper().get_fold_lvl(prev_block)
+        cont_line_regex = (r"(and|or|'|\+|\-|\*|\^|>>|<<|"
+                           r"\*|\*{2}|\||\*|//|/|,|\\|\")$")
         # round down to previous indentation guide to ensure contiguous block
         # fold level evolution.
         indent_len = 0
@@ -230,10 +233,22 @@ def detect_fold_level(self, prev_block, block):
             # ignore commented lines (could have arbitary indentation)
             prev_text = prev_block.text()
             indent_len = (len(prev_text) - len(prev_text.lstrip())) // prev_lvl
+            # Verify if the previous line ends with a continuation line
+            # with a regex
+            if (re.search(cont_line_regex, prev_block.text()) and
+                    indent_len > prev_lvl):
+                # Calculate act level of line
+                act_lvl = (len(text) - len(text.lstrip())) // indent_len
+                if act_lvl == prev_lvl:
+                    # If they are the same, don't change the level
+                    return prev_lvl
+                if prev_lvl > act_lvl:
+                    return prev_lvl - 1
+                return prev_lvl + 1
         if indent_len == 0:
             indent_len = len(self.editor.indent_chars)
-
-        return (len(text) - len(text.lstrip())) // indent_len
+        act_lvl = (len(text) - len(text.lstrip())) // indent_len
+        return act_lvl
 
 
 class CharBasedFoldDetector(FoldDetector):


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_folding.py b/spyder/plugins/editor/widgets/tests/test_folding.py
--- a/spyder/plugins/editor/widgets/tests/test_folding.py
+++ b/spyder/plugins/editor/widgets/tests/test_folding.py
@@ -9,49 +9,68 @@
 """
 
 # Third party imports
-from qtpy.QtGui import QTextCursor
 import pytest
 
 # Local imports
-from spyder.utils.qthelpers import qapplication
 from spyder.plugins.editor.widgets.codeeditor import CodeEditor
 from spyder.plugins.editor.api.folding import print_tree
 
+# ---Fixtures-----------------------------------------------------------------
+text = """
+def myfunc2():
+    x = [0, 1, 2, 3,
+        3 , 4] # Arbitary Code
+    x[0] = 2 # Desired break
+    print(x[1]) # Arbitary Code
 
-# --- Fixtures
-# -----------------------------------------------------------------------------
+responses = {
+    100: ('Continue', 'Request received, please continue'),
+    101: ('Switching Protocols','Switching to new protocol'),
+    200: ('OK', 'Request fulfilled, document follows'),
+    201: ('Created', 'Document created, URL follows'),
+    202: ('Accepted','Request accepted, processing continues off-line'),
+    203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
+    204: ('No Content', 'Request fulfilled, nothing follows'),
+    205: ('Reset Content', 'Clear input form for further input.'),
+    206: ('Partial Content', 'Partial content follows.'),
+    300: ('Multiple Choices','Object has several resources'),
+    301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
+    302: ('Found', 'Object moved temporarily -- see URI list'),
+    303: ('See Other', 'Object moved -- see Method and URL list'),
+    304: ('Not Modified',
+        'Document has not changed since given time'),
+    305: ('Use Proxy',
+        'You must use proxy specified in Location to access this ',
+        'resource.'),
+    307: ('Temporary Redirect',
+        'Object moved temporarily -- see URI list'),
+
+    400: ('Bad Request',
+        'Bad request syntax or unsupported method'),
+    401: ('Unauthorized',
+        'No permission -- see authorization schemes'),
+    402: ('Payment Required',
+        'No payment -- see charging schemes')
+}"""
+
+# --- Fixtures-----------------------------------------------------------------
 @pytest.fixture()
-def get_fold_levels():
+def code_editor():
     """setup editor and return fold levels."""
-    app = qapplication()
     editor = CodeEditor(parent=None)
     editor.setup_editor(language='Python')
-
-    text = ('# dummy test file\n'
-            'class a():\n'  # fold-block level-0
-            '    self.b = 1\n'
-            '    print(self.b)\n'
-            '    \n'
-            '    def some_method(self):\n'  # fold-block level-1
-            '        self.b = 3\n'
-            '\n'
-            '    def other_method(self):\n'  # fold-block level-1
-            '\n' # a blank line (should be ignored)
-            '  # a comment with arbitrary indentation\n' # should be ignored
-            '         a = (1,\n'  # fold-block level-2
-            '              2,\n'
-            '              3)\n'
-            )
-
     editor.set_text(text)
-    return print_tree(editor, return_list=True)
+    return editor
 
-# --- Tests
-# -----------------------------------------------------------------------------
-def test_simple_folding(get_fold_levels):
-    assert get_fold_levels == [[2, 0, 'V'],
-                                 [6, 1, 'V'],
-                                 [9, 1, 'V'],
-                                 [12, 2, 'V']]
 
+# --- Tests--------------------------------------------------------------------
+def test_simple_folding(code_editor):
+    """Test folding by the levels."""
+    expected_folding_levels = [0, 0, 1, 2, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1,
+                               1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1,
+                               2, 1, 2, 0]
 
+    output_fold = print_tree(code_editor, return_list=True)
+    for expected_lvl, (line, lvl, visible) in zip(expected_folding_levels,
+                                                  output_fold):
+        assert expected_lvl == lvl