Repository: spyder-ide/pyls-spyder

Issue:
Some issues with special comments in the outline explorer
After PR spyder-ide/spyder#13885, a "special comment" is shown in the outline explorer, regardless of the number of consecutive `-` following the `#`.

Before the outline explorer migration to the pyls, a special comment was registered only when there was exactly 4 consecutive `-` following the `#`. I think this rule should be added back in the current implementation of this feature.

For example, the outline explorer is showing special comments for the horizontal separator that are sometimes present in the header of spyder's file.

![image](https://user-images.githubusercontent.com/10170372/98265118-b3da2a80-1f56-11eb-8828-0361bb1cc8f9.png)

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 4.2.0.dev0 b7e9cf9b1
* Python version: 3.7.6 64-bit
* Qt version: 5.12.5
* PyQt5 version: 5.12.3
* Operating System: Windows 10


Comments:

Patch:
diff --git a/pyls_spyder/plugin.py b/pyls_spyder/plugin.py
index e185f03..e01c1c9 100644
--- a/pyls_spyder/plugin.py
+++ b/pyls_spyder/plugin.py
@@ -17,8 +17,35 @@ from pyls import hookimpl
 from pyls.config.config import Config
 from pyls.workspace import Workspace, Document
 
-CELL_REGEX = re.compile(r'^[\t ]*# (%%+)(.*)?$')
-BLOCK_REGEX = re.compile(r'^[\t ]*# (--+)(.*)?$')
+# Local imports
+from pyls_spyder.utils import RegexEvaluator
+
+# Code cell regular expressions
+# 1. Cells declared with percentages, i.e., # %% Cell
+CELL_PERCENTAGE, CELL_PERCENTAGE_REGEX = (
+    'CELL_PERCENTAGE', re.compile(r'^[\t ]*# ?(%%+)(.*)?$'))
+# 2. Cells declared with "<codecell>", i.e., # <codecell>
+CELL_CODECELL, CELL_CODECELL_REGEX = (
+    'CELL_CODECELL', re.compile(r'^[\t ]*# ?(<codecell>)(.*)?$'))
+# 3. Cells declared with "In[.*], i.e., # In[23]"
+CELL_IN, CELL_IN_REGEX = (
+    'CELL_IN', re.compile(r'^[\t ]*# ?(In\[)([^\]\r\n]*)?\]?$'))
+
+CELL_REGEX = RegexEvaluator({
+    CELL_PERCENTAGE: CELL_PERCENTAGE_REGEX,
+    CELL_CODECELL: CELL_CODECELL_REGEX,
+    CELL_IN: CELL_IN_REGEX
+})
+
+# Block comment regular expressions
+# 1. Block comments declared with 4 dashes, i.e., # ---- Block comment
+BLOCK_DASH = (
+    'BLOCK_DASH', re.compile(r'^[\t ]*# ?-{4}([^-\n\r]?.*)?$'))
+# 2. Block comments declared with 3 consecutive hashes, i.e., #### Comment
+BLOCK_HASH = (
+    'BLOCK_HASH', re.compile(r'^[\t ]*##{3}([^\#\n\r]?.*)?$'))
+
+BLOCK_REGEX = RegexEvaluator(dict([BLOCK_DASH, BLOCK_HASH]))
 
 
 def peek_symbol(list: List) -> Tuple:
@@ -70,8 +97,8 @@ def pyls_document_symbols(config: Config,
     unnamed_block = 1
 
     for line_num, line in enumerate(lines):
-        cell_match = CELL_REGEX.match(line)
-        block_match = BLOCK_REGEX.match(line)
+        cell_rule, cell_match = CELL_REGEX.match(line)
+        block_rule, block_match = BLOCK_REGEX.match(line)
 
         if cell_match is not None:
             percentages = cell_match.group(1)
@@ -81,7 +108,7 @@ def pyls_document_symbols(config: Config,
                 cell_name = 'Unnamed cell {0}'.format(unnamed_cell)
                 unnamed_cell += 1
 
-            if not group_cells:
+            if not group_cells or cell_rule != CELL_PERCENTAGE:
                 cells.append(create_symbol(
                     cell_name, document, line_num, line_num + 1))
             else:
@@ -99,7 +126,7 @@ def pyls_document_symbols(config: Config,
                             current_name) = peek_symbol(cell_stack)
                     cell_stack.insert(0, (line_num, cell_level, cell_name))
         elif block_match is not None and enable_block_comments:
-            block_name = block_match.group(2).strip()
+            block_name = block_match.group(1).strip()
             if block_name == '':
                 block_name = 'Unnamed comment {0}'.format(unnamed_block)
                 unnamed_block += 1
diff --git a/pyls_spyder/utils.py b/pyls_spyder/utils.py
new file mode 100644
index 0000000..94aef27
--- /dev/null
+++ b/pyls_spyder/utils.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# ----------------------------------------------------------------------------
+# Copyright (c) Spyder Project Contributors
+#
+# Licensed under the terms of the MIT License
+# (see LICENSE.txt for details)
+# ----------------------------------------------------------------------------
+
+"""pyls-spyder misc utillites."""
+
+# Standard library imports
+from typing import Tuple, Dict
+
+
+class RegexEvaluator:
+    """Wrapper class around multiple regular expressions."""
+
+    def __init__(self, regex_map: Dict):
+        self.regexes = regex_map
+
+    def match(self, string: str) -> Tuple:
+        """
+        Match a string `string` against a set of regular expressions.
+
+        The regular expressions are applied in a short-circuit fashion.
+
+        Parameters
+        ----------
+        string: str
+            Input string to match regexes against.
+
+        Returns
+        -------
+        output: Tuple[Optional[str], Optional[re.Match]]
+            A tuple containing the regex identifier that first matched the
+            input, alongside the corresponding regex match object. If no regex
+            did matched the input, then a tuple containing `None` is returned.
+        """
+        re_match = None
+        re_rule = None
+        for regex_name in self.regexes:
+            regex = self.regexes[regex_name]
+            re_match = regex.match(string)
+            if re_match is not None:
+                re_rule = regex_name
+                break
+        return re_rule, re_match


Test patch:
diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml
index e497b22..8e79031 100644
--- a/.github/workflows/linux-tests.yml
+++ b/.github/workflows/linux-tests.yml
@@ -22,7 +22,7 @@ jobs:
             - name: Checkout branch/PR
               uses: actions/checkout@v1
             - name: Install Conda
-              uses: goanpeca/setup-miniconda@v1
+              uses: conda-incubator/setup-miniconda@v2
               with:
                 activate-environment: test
                 auto-update-conda: true
diff --git a/pyls_spyder/tests/test_plugin.py b/pyls_spyder/tests/test_plugin.py
index 8582c49..8a8adc1 100644
--- a/pyls_spyder/tests/test_plugin.py
+++ b/pyls_spyder/tests/test_plugin.py
@@ -8,9 +8,6 @@
 
 """pyls-spyder plugin tests."""
 
-# Standard library imports
-import os
-
 # PyLS imports
 from pyls import uris
 from pyls.workspace import Document
@@ -26,12 +23,14 @@ from pyls_spyder.plugin import pyls_document_symbols
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """
 # %%
-# -- Imports
+# ---- Imports
 import os
 import sys
 
-# ------
+# <codecell> Other cell
+# ----
 def a():
+    #### Block comment on a
     # %%% Cell inside a
     for i in range(0, 10):
         # %%%% Cell
@@ -39,11 +38,14 @@ def a():
 
 # %%%
 def b():
-    # ----- Pass inside b
+    #---- Pass inside b
     pass
 
-# %% Empty cell
-# --
+# In[25]
+####
+
+#%% Empty cell
+#----
 """
 
 
@@ -61,15 +63,19 @@ def test_cell_block_symbols(config, workspace):
     document = Document(DOC_URI, workspace, DOC)
     symbols = pyls_document_symbols(config, workspace, document)
     expected = [
-        ('Unnamed cell 1', 1, 17, 225),
+        ('Unnamed cell 1', 1, 22, 225),
         ('Imports', 2, 2, 224),
-        ('Unnamed comment 1', 6, 6, 224),
-        ('Cell inside a', 8, 12, 225),
-        ('Cell', 10, 12, 225),
-        ('Unnamed cell 2', 13, 17, 225),
-        ('Pass inside b', 15, 15, 224),
-        ('Empty cell', 18, 19, 225),
-        ('Unnamed comment 2', 19, 19, 224)
+        ('Other cell', 6, 6, 225),
+        ('Unnamed comment 1', 7, 7, 224),
+        ('Block comment on a', 9, 9, 224),
+        ('Cell inside a', 10, 14, 225),
+        ('Cell', 12, 14, 225),
+        ('Unnamed cell 2', 15, 22, 225),
+        ('Pass inside b', 17, 17, 224),
+        ('25', 20, 20, 225),
+        ('Unnamed comment 2', 21, 21, 224),
+        ('Empty cell', 23, 24, 225),
+        ('Unnamed comment 3', 24, 24, 224)
     ]
     test_results = []
     for symbol in symbols:
@@ -90,13 +96,17 @@ def test_ungroup_cell_symbols(config, workspace):
     expected = [
         ('Unnamed cell 1', 1, 1, 225),
         ('Imports', 2, 2, 224),
-        ('Unnamed comment 1', 6, 6, 224),
-        ('Cell inside a', 8, 8, 225),
-        ('Cell', 10, 10, 225),
-        ('Unnamed cell 2', 13, 13, 225),
-        ('Pass inside b', 15, 15, 224),
-        ('Empty cell', 18, 18, 225),
-        ('Unnamed comment 2', 19, 19, 224)
+        ('Other cell', 6, 6, 225),
+        ('Unnamed comment 1', 7, 7, 224),
+        ('Block comment on a', 9, 9, 224),
+        ('Cell inside a', 10, 10, 225),
+        ('Cell', 12, 12, 225),
+        ('Unnamed cell 2', 15, 15, 225),
+        ('Pass inside b', 17, 17, 224),
+        ('25', 20, 20, 225),
+        ('Unnamed comment 2', 21, 21, 224),
+        ('Empty cell', 23, 23, 225),
+        ('Unnamed comment 3', 24, 24, 224)
     ]
     test_results = []
     for symbol in symbols:
@@ -115,11 +125,13 @@ def test_disable_block_comments(config, workspace):
     config.plugin_settings = lambda _: {'enable_block_comments': False}
     symbols = pyls_document_symbols(config, workspace, document)
     expected = [
-        ('Unnamed cell 1', 1, 17, 225),
-        ('Cell inside a', 8, 12, 225),
-        ('Cell', 10, 12, 225),
-        ('Unnamed cell 2', 13, 17, 225),
-        ('Empty cell', 18, 19, 225)
+        ('Unnamed cell 1', 1, 22, 225),
+        ('Other cell', 6, 6, 225),
+        ('Cell inside a', 10, 14, 225),
+        ('Cell', 12, 14, 225),
+        ('Unnamed cell 2', 15, 22, 225),
+        ('25', 20, 20, 225),
+        ('Empty cell', 23, 24, 225)
     ]
     test_results = []
     for symbol in symbols: