Repository: spyder-ide/spyder

Issue:
Outline Explorer: disable `if/else/try/for` statements
The Outline Explorer is really useful to jump to certain parts of the code quickly -- especially to functions, classes, code cells, and even comments starting with `# ---`. Especially the latter two help to structure a longer script better. However, the Outline Explorer also shows _every_ `if/else/try/for` which makes the outline basically useless: for a rather long file, the actual main structures (functions, cells, etc.) are somewhere in between several `if/else/try/for` code statements that are rather meaningless for an actual "outline" (especially the single `else:` lines).

For this reason I ask whether you can implement an additional option in the right-click menu like "Show if/else/try/for... statements" ... just as there exists "Show special comments" to toggle code cell and `# ---` comments. Thanks for considering!


Discussion:
I can second that; this would indeed be very useful. In general, I'm not sure I understand what general reason there would be to give if/else/try/catch substantially more importance than any other line of code, at least on the level of functions, classes, cells, etc, particularly without any option to disable it. I wouldn't think this would be *too* difficult to do...
If @ccordoba12 accept it, I can take a look at it since it is something that has been bothering me for some time now.
Jean, I think we should remove the code that shows `if/else/try/for` in the Outline Explorer. It's something that bothers me too, but I haven't had the time to fix it.
Perfect @ccordoba12 , consider it done :)

Resolving patch:
diff --git a/spyder/widgets/editortools.py b/spyder/widgets/editortools.py
--- a/spyder/widgets/editortools.py
+++ b/spyder/widgets/editortools.py
@@ -331,6 +331,10 @@ def __sort_toplevel_items(self):
         self.sort_top_level_items(key=sort_func)
             
     def populate_branch(self, editor, root_item, tree_cache=None):
+        """
+        Generates an outline of the editor's content and stores the result
+        in a cache.
+        """
         if tree_cache is None:
             tree_cache = {}
         
@@ -351,10 +355,7 @@ def populate_branch(self, editor, root_item, tree_cache=None):
         for block_nb in range(editor.get_line_count()):
             line_nb = block_nb+1
             data = oe_data.get(block_nb)
-            if data is None:
-                level = None
-            else:
-                level = data.fold_level
+            level = None if data is None else data.fold_level
             citem, clevel, _d = tree_cache.get(line_nb, (None, None, ""))
             
             # Skip iteration if line is not the first line of a foldable block
@@ -373,7 +374,13 @@ def populate_branch(self, editor, root_item, tree_cache=None):
                         if citem is not None:
                             remove_from_tree_cache(tree_cache, line=line_nb)
                         continue
-                
+
+            # Skip iteration for if/else/try/for/etc foldable blocks.
+            if not_class_nor_function and not data.is_comment():
+                if citem is not None:
+                    remove_from_tree_cache(tree_cache, line=line_nb)
+                continue
+
             if previous_level is not None:
                 if level == previous_level:
                     pass
@@ -391,8 +398,8 @@ def populate_branch(self, editor, root_item, tree_cache=None):
                 cname = to_text_string(citem.text(0))
                 
             preceding = root_item if previous_item is None else previous_item
-            if not_class_nor_function:
-                if data.is_comment() and not self.show_comments:
+            if not_class_nor_function and data.is_comment():
+                if not self.show_comments:
                     if citem is not None:
                         remove_from_tree_cache(tree_cache, line=line_nb)
                     continue
@@ -403,14 +410,10 @@ def populate_branch(self, editor, root_item, tree_cache=None):
                         continue
                     else:
                         remove_from_tree_cache(tree_cache, line=line_nb)
-                if data.is_comment():
-                    if data.def_type == data.CELL:
-                        item = CellItem(data.text, line_nb, parent, preceding)
-                    else:
-                        item = CommentItem(
-                            data.text, line_nb, parent, preceding)
+                if data.def_type == data.CELL:
+                    item = CellItem(data.text, line_nb, parent, preceding)
                 else:
-                    item = TreeItem(data.text, line_nb, parent, preceding)
+                    item = CommentItem(data.text, line_nb, parent, preceding)
             elif class_name is not None:
                 if citem is not None:
                     if class_name == cname and level == clevel:


Test patch:
diff --git a/spyder/widgets/tests/test_editortools.py b/spyder/widgets/tests/test_editortools.py
new file mode 100644
--- /dev/null
+++ b/spyder/widgets/tests/test_editortools.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+
+"""
+Tests for editortool.py
+"""
+
+# Third party imports
+import pytest
+
+# Local imports
+from spyder.widgets.editortools import (OutlineExplorerWidget, FileRootItem,
+                                        FunctionItem, CommentItem, CellItem,
+                                        ClassItem)
+from spyder.widgets.sourcecode.codeeditor import CodeEditor
+
+
+text = ("# -*- coding: utf-8 -*-\n"
+        "\n"
+        "# %% functions\n"
+        "\n"
+        "\n"
+        "# ---- func 1 and 2\n"
+        "\n"
+        "def func1():\n"
+        "    for i in range(3):\n"
+        "        print(i)\n"
+        "\n"
+        "\n"
+        "def func2():\n"
+        "    if True:\n"
+        "        pass\n"
+        "\n"
+        "\n"
+        "# ---- func 3\n"
+        "\n"
+        "def func3():\n"
+        "    pass\n"
+        "\n"
+        "\n"
+        "# %% classes\n"
+        "\n"
+        "class class1(object):\n"
+        "\n"
+        "    def __ini__(self):\n"
+        "        super(class1, self).__init__()\n"
+        "\n"
+        "    def method1(self):\n"
+        "        if False:\n"
+        "            pass\n"
+        "\n"
+        "    def method2(self):\n"
+        "        try:\n"
+        "            assert 2 == 3\n"
+        "        except AssertionError:\n"
+        "            pass")
+
+
+# Qt Test Fixtures
+# --------------------------------
+
+@pytest.fixture
+def outline_explorer_bot(qtbot):
+    editor = CodeEditor()
+    editor = CodeEditor(None)
+    editor.set_language('py', 'test_outline_explorer.py')
+    editor.set_text(text)
+
+    outline_explorer = OutlineExplorerWidget()
+    outline_explorer.set_current_editor(
+            editor, 'test_outline_explorer.py', False, False)
+
+    qtbot.addWidget(outline_explorer)
+
+    return outline_explorer, qtbot
+
+
+# Test OutlineExplorerWidget
+# -------------------------------
+
+def test_outline_explorer(outline_explorer_bot):
+    """
+    Test to assert the outline explorer is initializing correctly and
+    is showing the expected number of items, the expected type of items, and
+    the expected text for each item.
+    """
+    outline_explorer, qtbot = outline_explorer_bot
+    outline_explorer.show()
+    outline_explorer.setFixedSize(400, 350)
+    assert outline_explorer
+
+    outline_explorer.treewidget.expandAll()
+    tree_widget = outline_explorer.treewidget
+    all_items = tree_widget.get_top_level_items() + tree_widget.get_items()
+
+    # Assert that the expected number, text and type of items is displayed in
+    # the tree.
+    expected_results = [('test_outline_explorer.py', FileRootItem),
+                        ('functions', CellItem),
+                        ('---- func 1 and 2', CommentItem),
+                        ('func1', FunctionItem, False),
+                        ('func2', FunctionItem, False),
+                        ('---- func 3', CommentItem),
+                        ('func3', FunctionItem, False),
+                        ('classes', CellItem),
+                        ('class1', ClassItem),
+                        ('__ini__', FunctionItem, True),
+                        ('method1', FunctionItem, True),
+                        ('method2', FunctionItem, True)]
+
+    assert len(all_items) == len(expected_results)
+    for item, expected_result in zip(all_items, expected_results):
+        assert item.text(0) == expected_result[0]
+        assert type(item) == expected_result[1]
+        if type(item) == FunctionItem:
+            assert item.is_method() == expected_result[2]
+
+
+if __name__ == "__main__":
+    import os
+    pytest.main(['-x', os.path.basename(__file__), '-v', '-rw'])