Repository: sublimelsp/LSP

Issue:
Go To Symbol - Ability to filter the search (e.g: only structs or functions)
**Describe the solution you'd like**
I would like to be able to filter out by symbol type on the "Go To Symbol" command.

**Additional context**
I most likely do not want struct *fields* listed here but they populate most of the list


Discussion:
I did it. i'll make a PR with a LSP setting later.
I don't think we ever got the PR?

But I would anyway discuss this more before (or within the PR) because I'm not sure we want the extra complexity of additional option. Also, when would the option apply? Globally, per server, per request?

Resolving patch:
diff --git a/plugin/core/views.py b/plugin/core/views.py
--- a/plugin/core/views.py
+++ b/plugin/core/views.py
@@ -193,27 +193,6 @@
     sublime.KIND_VARIABLE: "entity.name.constant | constant.other | support.constant | variable.other | variable.parameter | variable.other.member | variable.other.readwrite.member"  # noqa: E501
 }  # type: Dict[SublimeKind, str]
 
-# Recommended colors to use by themes for each symbol kind, based on the kind_container specialization class described
-# at https://www.sublimetext.com/docs/themes.html#quick-panel
-SUBLIME_KIND_ID_COLOR_SCOPES = {
-    sublime.KIND_ID_KEYWORD: "region.pinkish",
-    sublime.KIND_ID_TYPE: "region.purplish",
-    sublime.KIND_ID_FUNCTION: "region.redish",
-    sublime.KIND_ID_NAMESPACE: "region.bluish",
-    sublime.KIND_ID_NAVIGATION: "region.yellowish",
-    sublime.KIND_ID_MARKUP: "region.orangish",
-    sublime.KIND_ID_VARIABLE: "region.cyanish",
-    sublime.KIND_ID_SNIPPET: "region.greenish",
-    sublime.KIND_ID_COLOR_REDISH: "region.redish",
-    sublime.KIND_ID_COLOR_ORANGISH: "region.orangish",
-    sublime.KIND_ID_COLOR_YELLOWISH: "region.yellowish",
-    sublime.KIND_ID_COLOR_GREENISH: "region.greenish",
-    sublime.KIND_ID_COLOR_CYANISH: "region.cyanish",
-    sublime.KIND_ID_COLOR_BLUISH: "region.bluish",
-    sublime.KIND_ID_COLOR_PURPLISH: "region.purplish",
-    sublime.KIND_ID_COLOR_PINKISH: "region.pinkish"
-}  # type: Dict[int, str]
-
 DOCUMENT_HIGHLIGHT_KINDS = {
     DocumentHighlightKind.Text: "text",
     DocumentHighlightKind.Read: "read",
diff --git a/plugin/goto_diagnostic.py b/plugin/goto_diagnostic.py
--- a/plugin/goto_diagnostic.py
+++ b/plugin/goto_diagnostic.py
@@ -184,7 +184,7 @@ def next_input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler]
         if diagnostic is None:
             self._preview = None
             return DiagnosticInputHandler(self.window, self.view, uri)
-        return sublime_plugin.BackInputHandler()  # type: ignore
+        return sublime_plugin.BackInputHandler()
 
     def confirm(self, value: Optional[DocumentUri]) -> None:
         self.uri = value
diff --git a/plugin/symbols.py b/plugin/symbols.py
--- a/plugin/symbols.py
+++ b/plugin/symbols.py
@@ -1,14 +1,18 @@
 import weakref
-from .core.protocol import Request, DocumentSymbol, SymbolInformation, SymbolKind, SymbolTag
+from .core.protocol import DocumentSymbol
+from .core.protocol import DocumentSymbolParams
+from .core.protocol import Request
+from .core.protocol import SymbolInformation
+from .core.protocol import SymbolKind
+from .core.protocol import SymbolTag
 from .core.registry import LspTextCommand
 from .core.sessions import print_to_status_bar
-from .core.typing import Any, List, Optional, Tuple, Dict, Generator, Union, cast
+from .core.typing import Any, List, Optional, Tuple, Dict, Union, cast
 from .core.views import range_to_region
-from .core.views import SUBLIME_KIND_ID_COLOR_SCOPES
 from .core.views import SublimeKind
 from .core.views import SYMBOL_KINDS
 from .core.views import text_document_identifier
-from contextlib import contextmanager
+from .goto_diagnostic import PreselectedListInputHandler
 import os
 import sublime
 import sublime_plugin
@@ -16,15 +20,40 @@
 
 SUPPRESS_INPUT_SETTING_KEY = 'lsp_suppress_input'
 
+SYMBOL_KIND_NAMES = {
+    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.EnumMember: "Enum Member",
+    SymbolKind.Struct: "Struct",
+    SymbolKind.Event: "Event",
+    SymbolKind.Operator: "Operator",
+    SymbolKind.TypeParameter: "Type Parameter"
+}  # type: Dict[SymbolKind, str]
+
 
 def unpack_lsp_kind(kind: SymbolKind) -> SublimeKind:
     return SYMBOL_KINDS.get(kind, sublime.KIND_AMBIGUOUS)
 
 
-def get_symbol_color_scope_from_lsp_kind(kind: SymbolKind) -> str:
-    return SUBLIME_KIND_ID_COLOR_SCOPES.get(unpack_lsp_kind(kind)[0], "comment")
-
-
 def symbol_information_to_quick_panel_item(
     item: SymbolInformation,
     show_file_name: bool = True
@@ -47,11 +76,36 @@ def symbol_information_to_quick_panel_item(
             kind=(st_kind, st_icon, st_display_type))
 
 
-@contextmanager
-def _additional_name(names: List[str], name: str) -> Generator[None, None, None]:
-    names.append(name)
-    yield
-    names.pop(-1)
+def symbol_to_list_input_item(
+    view: sublime.View, item: Union[DocumentSymbol, SymbolInformation], hierarchy: str = ''
+) -> sublime.ListInputItem:
+    name = item['name']
+    kind = item['kind']
+    st_kind = SYMBOL_KINDS.get(kind, sublime.KIND_AMBIGUOUS)
+    details = []
+    selection_range = item.get('selectionRange')
+    if selection_range:
+        item = cast(DocumentSymbol, item)
+        detail = item.get('detail')
+        if detail:
+            details.append(detail)
+        if hierarchy:
+            details.append(hierarchy + " > " + name)
+        region = range_to_region(selection_range, view)
+    else:
+        item = cast(SymbolInformation, item)
+        container_name = item.get('containerName')
+        if container_name:
+            details.append(container_name)
+        region = range_to_region(item['location']['range'], view)
+    deprecated = SymbolTag.Deprecated in (item.get('tags') or []) or item.get('deprecated', False)
+    return sublime.ListInputItem(
+        name,
+        {'kind': kind, 'region': [region.a, region.b], 'deprecated': deprecated},
+        details=" • ".join(details),
+        annotation=st_kind[2],
+        kind=st_kind
+    )
 
 
 class LspSelectionClearCommand(sublime_plugin.TextCommand):
@@ -84,143 +138,171 @@ def run(self, _: sublime.Edit, regions: List[Tuple[int, int]]) -> None:
 class LspDocumentSymbolsCommand(LspTextCommand):
 
     capability = 'documentSymbolProvider'
-    REGIONS_KEY = 'lsp_document_symbols'
 
     def __init__(self, view: sublime.View) -> None:
         super().__init__(view)
-        self.old_regions = []  # type: List[sublime.Region]
-        self.regions = []  # type: List[Tuple[sublime.Region, Optional[sublime.Region], str]]
-
-    def run(self, edit: sublime.Edit, event: Optional[Dict[str, Any]] = None) -> None:
-        self.view.settings().set(SUPPRESS_INPUT_SETTING_KEY, True)
-        session = self.best_session(self.capability)
-        if session:
-            session.send_request(
-                Request.documentSymbols({"textDocument": text_document_identifier(self.view)}, self.view),
-                lambda response: sublime.set_timeout(lambda: self.handle_response(response)),
-                lambda error: sublime.set_timeout(lambda: self.handle_response_error(error)))
-
-    def handle_response(self, response: Union[List[DocumentSymbol], List[SymbolInformation], None]) -> None:
+        self.items = []  # type: List[sublime.ListInputItem]
+        self.kind = 0
+        self.cached = False
+        self.has_matching_symbols = True
+
+    def run(
+        self,
+        edit: sublime.Edit,
+        event: Optional[Dict[str, Any]] = None,
+        kind: int = 0,
+        index: Optional[int] = None
+    ) -> None:
+        if index is None:
+            if not self.has_matching_symbols:
+                self.has_matching_symbols = True
+                window = self.view.window()
+                if window:
+                    kind_name = SYMBOL_KIND_NAMES.get(cast(SymbolKind, self.kind))
+                    window.status_message('No symbols of kind "{}" in this file'.format(kind_name))
+                return
+            self.kind = kind
+            session = self.best_session(self.capability)
+            if session:
+                self.view.settings().set(SUPPRESS_INPUT_SETTING_KEY, True)
+                params = {"textDocument": text_document_identifier(self.view)}  # type: DocumentSymbolParams
+                session.send_request(
+                    Request.documentSymbols(params, self.view), self.handle_response_async, self.handle_response_error)
+
+    def input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler]:
+        if self.cached:
+            self.cached = False
+            if self.kind and not any(item.value['kind'] == self.kind for item in self.items):
+                self.has_matching_symbols = False
+                return None
+            window = self.view.window()
+            if not window:
+                return None
+            symbol_kind = cast(SymbolKind, self.kind)
+            initial_value = sublime.ListInputItem(
+                SYMBOL_KIND_NAMES.get(symbol_kind, 'All Kinds'),
+                self.kind,
+                kind=SYMBOL_KINDS.get(symbol_kind, sublime.KIND_AMBIGUOUS))
+            return DocumentSymbolsKindInputHandler(window, initial_value, self.view, self.items)
+        return None
+
+    def handle_response_async(self, response: Union[List[DocumentSymbol], List[SymbolInformation], None]) -> None:
         self.view.settings().erase(SUPPRESS_INPUT_SETTING_KEY)
-        window = self.view.window()
-        if window and isinstance(response, list) and len(response) > 0:
-            panel_items = self.process_symbols(response)
-            self.old_regions = [sublime.Region(r.a, r.b) for r in self.view.sel()]
-            # Find region that is either intersecting or before to the current selection end.
-            selected_index = 0
-            if self.old_regions:
-                first_selection = self.old_regions[0]
-                for i, (r, _, _) in enumerate(self.regions):
-                    if r.begin() <= first_selection.b:
-                        selected_index = i
-                    else:
-                        break
-            self.view.run_command("lsp_selection_clear")
-            window.show_quick_panel(
-                panel_items,
-                self.on_symbol_selected,
-                sublime.KEEP_OPEN_ON_FOCUS_LOST,
-                selected_index,
-                self.on_highlighted)
+        self.items.clear()
+        if response and self.view.is_valid():
+            if 'selectionRange' in response[0]:
+                items = cast(List[DocumentSymbol], response)
+                for item in items:
+                    self.items.extend(self.process_document_symbol_recursive(item))
+            else:
+                items = cast(List[SymbolInformation], response)
+                for item in items:
+                    self.items.append(symbol_to_list_input_item(self.view, item))
+            self.items.sort(key=lambda item: item.value['region'])
+            window = self.view.window()
+            if window:
+                self.cached = True
+                window.run_command('show_overlay', {'overlay': 'command_palette', 'command': 'lsp_document_symbols'})
 
     def handle_response_error(self, error: Any) -> None:
         self.view.settings().erase(SUPPRESS_INPUT_SETTING_KEY)
         print_to_status_bar(error)
 
-    def region(self, index: int) -> sublime.Region:
-        return self.regions[index][0]
-
-    def selection_region(self, index: int) -> Optional[sublime.Region]:
-        return self.regions[index][1]
-
-    def scope(self, index: int) -> str:
-        return self.regions[index][2]
+    def process_document_symbol_recursive(
+        self, item: DocumentSymbol, hierarchy: str = ''
+    ) -> List[sublime.ListInputItem]:
+        name = item['name']
+        name_hierarchy = hierarchy + " > " + name if hierarchy else name
+        items = [symbol_to_list_input_item(self.view, item, hierarchy)]
+        for child in item.get('children') or []:
+            items.extend(self.process_document_symbol_recursive(child, name_hierarchy))
+        return items
+
+
+class DocumentSymbolsKindInputHandler(PreselectedListInputHandler):
+
+    def __init__(
+        self,
+        window: sublime.Window,
+        initial_value: sublime.ListInputItem,
+        view: sublime.View,
+        items: List[sublime.ListInputItem],
+    ) -> None:
+        super().__init__(window, initial_value)
+        self.view = view
+        self.items = items
+        self.old_selection = [sublime.Region(r.a, r.b) for r in view.sel()]
+        self.last_selected = 0
+
+    def name(self) -> str:
+        return 'kind'
 
-    def on_symbol_selected(self, index: int) -> None:
-        if index == -1:
-            if self.old_regions:
-                self.view.run_command("lsp_selection_set", {"regions": [(r.a, r.b) for r in self.old_regions]})
-                self.view.show_at_center(self.old_regions[0].begin())
-        else:
-            region = self.selection_region(index)
-            if not region:
-                self.view.erase_regions(self.REGIONS_KEY)
-                region = self.region(index)
-                self.view.run_command("lsp_selection_set", {"regions": [(region.a, region.a)]})
-            self.view.show_at_center(region.a)
-        self.old_regions.clear()
-        self.regions.clear()
-
-    def on_highlighted(self, index: int) -> None:
-        region = self.selection_region(index)
-        if region:
-            self.view.run_command("lsp_selection_set", {"regions": [region.to_tuple()]})
-        else:
-            region = self.region(index)
-            self.view.add_regions(self.REGIONS_KEY, [region], self.scope(index), '', sublime.DRAW_NO_FILL)
-        self.view.show_at_center(region.a)
-
-    def process_symbols(
-            self,
-            items: Union[List[DocumentSymbol], List[SymbolInformation]]
-    ) -> List[sublime.QuickPanelItem]:
-        self.regions.clear()
-        panel_items = []
-        if 'selectionRange' in items[0]:
-            items = cast(List[DocumentSymbol], items)
-            panel_items = self.process_document_symbols(items)
+    def placeholder(self) -> str:
+        return "Symbol Kind"
+
+    def get_list_items(self) -> Tuple[List[sublime.ListInputItem], int]:
+        items = [sublime.ListInputItem('All Kinds', 0, kind=sublime.KIND_AMBIGUOUS)]
+        items.extend([
+            sublime.ListInputItem(SYMBOL_KIND_NAMES[lsp_kind], lsp_kind, kind=st_kind)
+            for lsp_kind, st_kind in SYMBOL_KINDS.items()
+            if any(item.value['kind'] == lsp_kind for item in self.items)
+        ])
+        for index, item in enumerate(items):
+            if item.value == self.last_selected:
+                break
         else:
-            items = cast(List[SymbolInformation], items)
-            panel_items = self.process_symbol_informations(items)
-        # Sort both lists in sync according to the range's begin point.
-        sorted_results = zip(*sorted(zip(self.regions, panel_items), key=lambda item: item[0][0].begin()))
-        sorted_regions, sorted_panel_items = sorted_results
-        self.regions = list(sorted_regions)  # type: ignore
-        return list(sorted_panel_items)  # type: ignore
-
-    def process_document_symbols(self, items: List[DocumentSymbol]) -> List[sublime.QuickPanelItem]:
-        quick_panel_items = []  # type: List[sublime.QuickPanelItem]
-        names = []  # type: List[str]
-        for item in items:
-            self.process_document_symbol_recursive(quick_panel_items, item, names)
-        return quick_panel_items
-
-    def process_document_symbol_recursive(self, quick_panel_items: List[sublime.QuickPanelItem], item: DocumentSymbol,
-                                          names: List[str]) -> None:
-        lsp_kind = item["kind"]
-        self.regions.append((range_to_region(item['range'], self.view),
-                             range_to_region(item['selectionRange'], self.view),
-                             get_symbol_color_scope_from_lsp_kind(lsp_kind)))
-        name = item['name']
-        with _additional_name(names, name):
-            st_kind, st_icon, st_display_type = unpack_lsp_kind(lsp_kind)
-            formatted_names = " > ".join(names)
-            st_details = item.get("detail") or ""
-            if st_details:
-                st_details = "{} | {}".format(st_details, formatted_names)
-            else:
-                st_details = formatted_names
-            tags = item.get("tags") or []
-            if SymbolTag.Deprecated in tags:
-                st_display_type = "⚠ {} - Deprecated".format(st_display_type)
-            quick_panel_items.append(
-                sublime.QuickPanelItem(
-                    trigger=name,
-                    details=st_details,
-                    annotation=st_display_type,
-                    kind=(st_kind, st_icon, st_display_type)))
-            children = item.get('children') or []  # type: List[DocumentSymbol]
-            for child in children:
-                self.process_document_symbol_recursive(quick_panel_items, child, names)
-
-    def process_symbol_informations(self, items: List[SymbolInformation]) -> List[sublime.QuickPanelItem]:
-        quick_panel_items = []  # type: List[sublime.QuickPanelItem]
-        for item in items:
-            self.regions.append((range_to_region(item['location']['range'], self.view),
-                                 None, get_symbol_color_scope_from_lsp_kind(item['kind'])))
-            quick_panel_item = symbol_information_to_quick_panel_item(item, show_file_name=False)
-            quick_panel_items.append(quick_panel_item)
-        return quick_panel_items
+            index = 0
+        return items, index
+
+    def confirm(self, text: int) -> None:
+        self.last_selected = text
+
+    def next_input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler]:
+        kind = args.get('kind')
+        if kind is not None:
+            return DocumentSymbolsInputHandler(self.view, kind, self.items, self.old_selection)
+
+
+class DocumentSymbolsInputHandler(sublime_plugin.ListInputHandler):
+
+    def __init__(
+        self, view: sublime.View, kind: int, items: List[sublime.ListInputItem], old_selection: List[sublime.Region]
+    ) -> None:
+        super().__init__()
+        self.view = view
+        self.kind = kind
+        self.items = items
+        self.old_selection = old_selection
+
+    def name(self) -> str:
+        return 'index'
+
+    def list_items(self) -> Tuple[List[sublime.ListInputItem], int]:
+        items = [item for item in self.items if not self.kind or item.value['kind'] == self.kind]
+        selected_index = 0
+        if self.old_selection:
+            pt = self.old_selection[0].b
+            for index, item in enumerate(items):
+                if item.value['region'][0] <= pt:
+                    selected_index = index
+                else:
+                    break
+        return items, selected_index
+
+    def preview(self, text: Any) -> Union[str, sublime.Html, None]:
+        if isinstance(text, dict):
+            r = text.get('region')
+            if r:
+                self.view.run_command('lsp_selection_set', {'regions': [(r[0], r[1])]})
+                self.view.show_at_center(r[0])
+            if text.get('deprecated'):
+                return "⚠ Deprecated"
+        return ""
+
+    def cancel(self) -> None:
+        if self.old_selection:
+            self.view.run_command('lsp_selection_set', {'regions': [(r.a, r.b) for r in self.old_selection]})
+            self.view.show_at_center(self.old_selection[0].begin())
 
 
 class SymbolQueryInput(sublime_plugin.TextInputHandler):


Test patch: