Repository: spyder-ide/spyder

Issue:
Move symbol switcher to use LSP symbols
Right now we're extracting symbols to show in the switcher using the Python syntax highlighter, which degrades performance in the Editor for large files.

The plan is to remove that functionality and populate the switcher using the LSP symbols instead.


Discussion:

Resolving patch:
diff --git a/spyder/plugins/completion/languageserver/__init__.py b/spyder/plugins/completion/languageserver/__init__.py
--- a/spyder/plugins/completion/languageserver/__init__.py
+++ b/spyder/plugins/completion/languageserver/__init__.py
@@ -595,3 +595,40 @@ class ClientConstants:
 class WorkspaceUpdateKind:
     ADDITION = 'addition'
     DELETION = 'deletion'
+
+
+# ----------------------- Symbol kind icon names --------------------
+# Additional symbol constants (non-standard)
+BLOCK_COMMENT = 224
+CELL = 225
+
+SYMBOL_KIND_ICON = {
+    SymbolKind.FILE: 'file',
+    SymbolKind.MODULE: 'module',
+    SymbolKind.NAMESPACE: 'namespace',
+    SymbolKind.PACKAGE: 'package',
+    SymbolKind.CLASS: 'class',
+    SymbolKind.METHOD: 'method',
+    SymbolKind.PROPERTY: 'property',
+    SymbolKind.FIELD: 'field',
+    SymbolKind.CONSTRUCTOR: 'constructor',
+    SymbolKind.ENUM: 'enum',
+    SymbolKind.INTERFACE: 'interface',
+    SymbolKind.FUNCTION: 'function',
+    SymbolKind.VARIABLE: 'variable',
+    SymbolKind.CONSTANT: 'constant',
+    SymbolKind.STRING: 'string',
+    SymbolKind.NUMBER: 'number',
+    SymbolKind.BOOLEAN: 'boolean',
+    SymbolKind.ARRAY: 'array',
+    SymbolKind.OBJECT: 'object',
+    SymbolKind.KEY: 'key',
+    SymbolKind.NULL: 'null',
+    SymbolKind.ENUM_MEMBER: 'enum_member',
+    SymbolKind.STRUCT: 'struct',
+    SymbolKind.EVENT: 'event',
+    SymbolKind.OPERATOR: 'operator',
+    SymbolKind.TYPE_PARAMETER: 'type_parameter',
+    BLOCK_COMMENT: 'blockcomment',
+    CELL: 'cell'
+}
diff --git a/spyder/plugins/editor/utils/switcher.py b/spyder/plugins/editor/utils/switcher.py
--- a/spyder/plugins/editor/utils/switcher.py
+++ b/spyder/plugins/editor/utils/switcher.py
@@ -16,76 +16,11 @@
 
 # Local imports
 from spyder.config.base import _
+from spyder.config.manager import CONF
 from spyder.utils import icon_manager as ima
 from spyder.utils.switcher import shorten_paths, get_file_icon
-
-
-def get_symbol_list(outlineexplorer_data_list):
-    """
-    Get the list of symbols present in the outline explorer data list.
-
-    Returns a list with line number, definition name, fold and token.
-    """
-    symbol_list = []
-    for oedata in outlineexplorer_data_list:
-        if oedata.is_class_or_function():
-            line_number = oedata.get_block_number()
-            if line_number is not None:
-                symbol_list.append((
-                    line_number + 1,
-                    oedata.def_name,
-                    oedata.fold_level,
-                    oedata.get_token()))
-    return sorted(symbol_list)
-
-
-def get_python_symbol_icons(symbols):
-    """Return a list of icons for symbols of a python file."""
-    class_icon = ima.icon('class')
-    method_icon = ima.icon('method')
-    function_icon = ima.icon('function')
-    private_icon = ima.icon('private1')
-    super_private_icon = ima.icon('private2')
-
-    # line - 1, name, fold level
-    fold_levels = sorted(list(set([s[2] for s in symbols])))
-    parents = [None]*len(symbols)
-    icons = [None]*len(symbols)
-    indexes = []
-
-    parent = None
-    for level in fold_levels:
-        for index, item in enumerate(symbols):
-            line, name, fold_level, token = item
-            if index in indexes:
-                continue
-
-            if fold_level == level:
-                indexes.append(index)
-                parent = item
-            else:
-                parents[index] = parent
-
-    for index, item in enumerate(symbols):
-        parent = parents[index]
-
-        if item[-1] == 'def':
-            icons[index] = function_icon
-        elif item[-1] == 'class':
-            icons[index] = class_icon
-        else:
-            icons[index] = QIcon()
-
-        if parent is not None:
-            if parent[-1] == 'class':
-                if item[-1] == 'def' and item[1].startswith('__'):
-                    icons[index] = super_private_icon
-                elif item[-1] == 'def' and item[1].startswith('_'):
-                    icons[index] = private_icon
-                else:
-                    icons[index] = method_icon
-
-    return icons
+from spyder.plugins.completion.languageserver import (
+    SymbolKind, BLOCK_COMMENT, CELL, SYMBOL_KIND_ICON)
 
 
 class EditorSwitcherManager(object):
@@ -197,29 +132,52 @@ def create_line_switcher(self):
     def create_symbol_switcher(self):
         """Populate switcher with symbol info."""
         editor = self._editor()
+        language = editor.language
+        editor.update_whitespace_count(0, 0)
         self._current_line = editor.get_cursor_line_number()
         self._switcher.clear()
         self._switcher.set_placeholder_text(_('Select symbol'))
-        oedata_list = editor.outlineexplorer_data_list()
+        oe_symbols = editor.oe_proxy.info or []
+        display_variables = CONF.get('outline_explorer', 'display_variables')
+        group_cells = CONF.get('outline_explorer', 'group_cells')
+
+        idx = 0
+        total_symbols = len(oe_symbols)
+        oe_symbols = sorted(
+            oe_symbols, key=lambda x: x['location']['range']['start']['line'])
+        for symbol in oe_symbols:
+            symbol_name = symbol['name']
+            symbol_kind = symbol['kind']
+            if language.lower() == 'python':
+                if symbol_kind == SymbolKind.MODULE:
+                    total_symbols -= 1
+                    continue
+                if (symbol_kind == SymbolKind.VARIABLE and
+                        not display_variables):
+                    total_symbols -= 1
+                    continue
+                if symbol_kind == SymbolKind.FIELD and not display_variables:
+                    total_symbols -= 1
+                    continue
+
+            symbol_range = symbol['location']['range']
+            symbol_start = symbol_range['start']['line']
+
+            fold_level = editor.leading_whitespaces[symbol_start]
 
-        symbols_list = get_symbol_list(oedata_list)
-        icons = get_python_symbol_icons(symbols_list)
-        for idx, symbol in enumerate(symbols_list):
-            title = symbol[1]
-            fold_level = symbol[2]
             space = ' ' * fold_level
-            formated_title = '{space}{title}'.format(title=title,
+            formated_title = '{space}{title}'.format(title=symbol_name,
                                                      space=space)
-            line_number = symbol[0]
-            icon = icons[idx]
-            data = {'title': title,
-                    'line_number': line_number + 1}
-            last_item = idx + 1 == len(symbols_list)
+            icon = ima.icon(SYMBOL_KIND_ICON.get(symbol_kind, 'no_match'))
+            data = {'title': symbol_name,
+                    'line_number': symbol_start + 1}
+            last_item = idx + 1 == total_symbols
             self._switcher.add_item(title=formated_title,
                                     icon=icon,
                                     section=self._section,
                                     data=data,
                                     last_item=last_item)
+            idx += 1
         # Needed to update fold spaces for items titles
         self._switcher.setup()
 
diff --git a/spyder/plugins/outlineexplorer/widgets.py b/spyder/plugins/outlineexplorer/widgets.py
--- a/spyder/plugins/outlineexplorer/widgets.py
+++ b/spyder/plugins/outlineexplorer/widgets.py
@@ -23,46 +23,13 @@
 from spyder.config.manager import CONF
 from spyder.py3compat import to_text_string
 from spyder.utils import icon_manager as ima
-from spyder.plugins.completion.languageserver import SymbolKind
+from spyder.plugins.completion.languageserver import (
+    SymbolKind, BLOCK_COMMENT, CELL, SYMBOL_KIND_ICON)
 from spyder.utils.qthelpers import (create_action, create_toolbutton,
                                     set_item_user_text, create_plugin_layout,
                                     create_waitspinner)
 from spyder.widgets.onecolumntree import OneColumnTree
 
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
-
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
 
 SYMBOL_NAME_MAP = {
     SymbolKind.FILE: _('File'),


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -1879,6 +1879,7 @@ def test_tight_layout_option_for_inline_plot(main_window, qtbot, tmpdir):
 
 @flaky(max_runs=3)
 @pytest.mark.slow
+@pytest.mark.use_introspection
 def test_switcher(main_window, qtbot, tmpdir):
     """Test the use of shorten paths when necessary in the switcher."""
     switcher = main_window.switcher
@@ -1926,6 +1927,16 @@ def example_def_2():
 
     # Assert symbol switcher works
     main_window.editor.set_current_filename(str(file_a))
+
+    code_editor = main_window.editor.get_focus_widget()
+    with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
+        code_editor.document_did_open()
+
+    with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
+        code_editor.request_symbols()
+
+    qtbot.wait(9000)
+
     main_window.open_switcher()
     qtbot.keyClicks(switcher.edit, '@')
     qtbot.wait(200)
@@ -1959,7 +1970,10 @@ def test_edidorstack_open_switcher_dlg(main_window, tmpdir):
 
 @flaky(max_runs=3)
 @pytest.mark.slow
-def test_edidorstack_open_symbolfinder_dlg(main_window, qtbot, tmpdir):
+@pytest.mark.use_introspection
+@pytest.mark.skipif(not sys.platform.startswith('linux'),
+                    reason="It times out too much on Windows and macOS")
+def test_editorstack_open_symbolfinder_dlg(main_window, qtbot, tmpdir):
     """
     Test that the symbol finder is working as expected when called from the
     editorstack.
@@ -1977,6 +1991,15 @@ def example_def_2():
                ''')
     main_window.editor.load(str(file))
 
+    code_editor = main_window.editor.get_focus_widget()
+    with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
+        code_editor.document_did_open()
+
+    with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
+        code_editor.request_symbols()
+
+    qtbot.wait(5000)
+
     # Test that the symbol finder opens as expected from the editorstack.
     editorstack = main_window.editor.get_current_editorstack()
     assert editorstack.switcher_dlg is None