From 80e77db97e582c3cbf4fc98a963028e65ee3664d Mon Sep 17 00:00:00 2001 From: jwortmann Date: Tue, 2 Jan 2024 22:14:44 +0100 Subject: [PATCH] Experimental Workspace Symbols overhaul (#2333) --- plugin/core/constants.py | 2 + plugin/core/input_handlers.py | 209 +++++++++++++++++++++++++++++++++ plugin/core/protocol.py | 4 + plugin/core/sessions.py | 3 + plugin/core/typing.py | 12 ++ plugin/goto_diagnostic.py | 45 +------ plugin/symbols.py | 198 ++++++++++++++++++------------- tests/test_document_symbols.py | 24 ---- 8 files changed, 348 insertions(+), 149 deletions(-) create mode 100644 plugin/core/input_handlers.py delete mode 100644 tests/test_document_symbols.py diff --git a/plugin/core/constants.py b/plugin/core/constants.py index 2a1fa9dec..8d249e4aa 100644 --- a/plugin/core/constants.py +++ b/plugin/core/constants.py @@ -10,6 +10,8 @@ SublimeKind = Tuple[int, str, str] +ST_VERSION = int(sublime.version()) + # Keys for View.add_regions HOVER_HIGHLIGHT_KEY = 'lsp_hover_highlight' diff --git a/plugin/core/input_handlers.py b/plugin/core/input_handlers.py new file mode 100644 index 000000000..cfd414c4d --- /dev/null +++ b/plugin/core/input_handlers.py @@ -0,0 +1,209 @@ +from .constants import ST_VERSION +from .typing import Any, Callable, Dict, List, Optional, ParamSpec, Tuple, Union +from .typing import final +from abc import ABCMeta +from abc import abstractmethod +import functools +import sublime +import sublime_plugin +import time +import weakref + + +ListItemsReturn = Union[List[str], Tuple[List[str], int], List[Tuple[str, Any]], Tuple[List[Tuple[str, Any]], int], + List[sublime.ListInputItem], Tuple[List[sublime.ListInputItem], int]] + +P = ParamSpec('P') + + +def debounced(user_function: Callable[P, Any]) -> Callable[P, None]: + """ A decorator which debounces the calls to a function. + + Note that the return value of the function will be discarded, so it only makes sense to use this decorator for + functions that return None. The function will run on Sublime's main thread. + """ + DEBOUNCE_TIME = 0.5 # seconds + + @functools.wraps(user_function) + def wrapped_function(*args: P.args, **kwargs: P.kwargs) -> None: + def check_call_function() -> None: + target_time = getattr(wrapped_function, '_target_time', None) + if isinstance(target_time, float): + additional_delay = target_time - time.monotonic() + if additional_delay > 0: + setattr(wrapped_function, '_target_time', None) + sublime.set_timeout(check_call_function, int(additional_delay * 1000)) + return + delattr(wrapped_function, '_target_time') + user_function(*args, **kwargs) + if hasattr(wrapped_function, '_target_time'): + setattr(wrapped_function, '_target_time', time.monotonic() + DEBOUNCE_TIME) + return + setattr(wrapped_function, '_target_time', None) + sublime.set_timeout(check_call_function, int(DEBOUNCE_TIME * 1000)) + return wrapped_function + + +class PreselectedListInputHandler(sublime_plugin.ListInputHandler, metaclass=ABCMeta): + """ A ListInputHandler which can preselect a value. + + Subclasses of PreselectedListInputHandler must not implement the `list_items` method, but instead `get_list_items`, + i.e. just prepend `get_` to the regular `list_items` method. + + To create an instance of PreselectedListInputHandler pass the window to the constructor, and optionally a second + argument `initial_value` to preselect a value. Usually you then want to use the `next_input` method to push another + InputHandler onto the input stack. + + Inspired by https://github.com/sublimehq/sublime_text/issues/5507. + """ + + def __init__( + self, window: sublime.Window, initial_value: Optional[Union[str, sublime.ListInputItem]] = None + ) -> None: + super().__init__() + self._window = window + self._initial_value = initial_value + + @final + def list_items(self) -> ListItemsReturn: + if self._initial_value is not None: + sublime.set_timeout(self._select_and_reset) + return [self._initial_value], 0 # pyright: ignore[reportGeneralTypeIssues] + else: + return self.get_list_items() + + def _select_and_reset(self) -> None: + self._initial_value = None + if self._window.is_valid(): + self._window.run_command('select') + + @abstractmethod + def get_list_items(self) -> ListItemsReturn: + raise NotImplementedError() + + +class DynamicListInputHandler(sublime_plugin.ListInputHandler, metaclass=ABCMeta): + """ A ListInputHandler which can update its items while typing in the input field. + + Subclasses of PreselectedListInputHandler must not implement the `list_items` method, but can override + `get_list_items` for the initial list items. The `on_modified` method will be called after a small delay (debounced) + whenever changes were made to the input text. You can use this to call the `update` method with a list of + `ListInputItem`s to update the list items. + + To create an instance of the derived class pass the command instance and the command arguments to the constructor, + like this: + + def input(self, args): + return MyDynamicListInputHandler(self, args) + + For now, the type of the command must be a WindowCommand, but maybe it can be generalized later if needed. + This class will set and modify `_items` and '_text' attributes of the command, so make sure that those attribute + names are not used in another way in the command's class. + """ + + def __init__(self, command: sublime_plugin.WindowCommand, args: Dict[str, Any]) -> None: + super().__init__() + self.command = command + self.args = args + self.text = getattr(command, '_text', '') + self.listener = None # type: Optional[sublime_plugin.TextChangeListener] + self.input_view = None # type: Optional[sublime.View] + + def attach_listener(self) -> None: + for buffer in sublime._buffers(): # type: ignore + view = buffer.primary_view() + # This condition to find the input field view might not be sufficient if there is another command palette + # open in another group in the same window + if view.element() == 'command_palette:input' and view.window() == self.command.window: + self.input_view = view + break + else: + raise RuntimeError('Could not find the Command Palette input field view') + self.listener = InputListener(self) + self.listener.attach(buffer) + if ST_VERSION < 4161: + # Workaround for initial_selection not working; see https://github.com/sublimehq/sublime_text/issues/6175 + selection = self.input_view.sel() + selection.clear() + selection.add(len(self.text)) + + @final + def list_items(self) -> List[sublime.ListInputItem]: + if not self.text: # Show initial items when the command was just invoked + return self.get_list_items() or [sublime.ListInputItem("No Results", "")] + else: # Items were updated after typing + items = getattr(self.command, '_items', None) + if items: + if ST_VERSION >= 4157: + return items + else: + # Trick to select the topmost item; see https://github.com/sublimehq/sublime_text/issues/6162 + sublime.set_timeout(self._select_first_row) + return [sublime.ListInputItem("", "")] + items + return [sublime.ListInputItem('No Symbol found: "{}"'.format(self.text), "")] + + def _select_first_row(self) -> None: + self.command.window.run_command('move', {'by': 'lines', 'forward': True}) + + def initial_text(self) -> str: + setattr(self.command, '_text', '') + sublime.set_timeout(self.attach_listener) + return self.text + + def initial_selection(self) -> List[Tuple[int, int]]: + pt = len(self.text) + return [(pt, pt)] + + def validate(self, text: str) -> bool: + return bool(text) + + def cancel(self) -> None: + if self.listener and self.listener.is_attached(): + self.listener.detach() + + def confirm(self, text: str) -> None: + if self.listener and self.listener.is_attached(): + self.listener.detach() + + def on_modified(self, text: str) -> None: + """ Called after changes have been made to the input, with the text of the input field passed as argument. """ + pass + + def get_list_items(self) -> List[sublime.ListInputItem]: + """ The list items which are initially shown. """ + return [] + + def update(self, items: List[sublime.ListInputItem]) -> None: + """ Call this method to update the list items. """ + if not self.input_view: + return + setattr(self.command, '_items', items) + text = self.input_view.substr(sublime.Region(0, self.input_view.size())) + setattr(self.command, '_text', text) + self.command.window.run_command('chain', { + 'commands': [ + # Note that the command palette changes its width after the update, due to the hide_overlay command + ['hide_overlay', {}], + [self.command.name(), self.args] + ] + }) + + +class InputListener(sublime_plugin.TextChangeListener): + + def __init__(self, handler: DynamicListInputHandler) -> None: + super().__init__() + self.weakhandler = weakref.ref(handler) + + @classmethod + def is_applicable(cls, buffer: sublime.Buffer) -> bool: + return False + + @debounced + def on_text_changed(self, changes: List[sublime.TextChange]) -> None: + handler = self.weakhandler() + if not handler: + return + view = self.buffer.primary_view() + if view and view.id(): + handler.on_modified(view.substr(sublime.Region(0, view.size()))) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 5a0748a7f..85f878800 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -6145,6 +6145,10 @@ def foldingRange(cls, params: FoldingRangeParams, view: sublime.View) -> 'Reques def workspaceSymbol(cls, params: WorkspaceSymbolParams) -> 'Request': return Request("workspace/symbol", params, None, progress=True) + @classmethod + def resolveWorkspaceSymbol(cls, params: WorkspaceSymbol) -> 'Request': + return Request('workspaceSymbol/resolve', params) + @classmethod def documentDiagnostic(cls, params: DocumentDiagnosticParams, view: sublime.View) -> 'Request': return Request('textDocument/diagnostic', params, view) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index b7487ea29..7435cb12b 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -463,6 +463,9 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor "workspaceFolders": True, "symbol": { "dynamicRegistration": True, # exceptional + "resolveSupport": { + "properties": ["location.range"] + }, "symbolKind": { "valueSet": symbol_kinds }, diff --git a/plugin/core/typing.py b/plugin/core/typing.py index cda3e92af..be399071a 100644 --- a/plugin/core/typing.py +++ b/plugin/core/typing.py @@ -8,6 +8,7 @@ from typing import cast from typing import Deque from typing import Dict + from typing import final from typing import Generator from typing import Generic from typing import IO @@ -18,6 +19,7 @@ from typing import Mapping from typing import NotRequired from typing import Optional + from typing import ParamSpec from typing import Protocol from typing import Required from typing import Sequence @@ -37,6 +39,9 @@ def cast(typ, val): # type: ignore return val + def final(func): # type: ignore + return func + def _make_type(name: str) -> '_TypeMeta': return _TypeMeta(name, (Type,), {}) # type: ignore @@ -138,3 +143,10 @@ class NotRequired(Type): # type: ignore def TypeVar(*args, **kwargs) -> Any: # type: ignore return object + + class ParamSpec(Type): # type: ignore + args = ... + kwargs = ... + + def __init__(*args, **kwargs) -> None: # type: ignore + pass diff --git a/plugin/goto_diagnostic.py b/plugin/goto_diagnostic.py index c478cbf06..f9d0abad7 100644 --- a/plugin/goto_diagnostic.py +++ b/plugin/goto_diagnostic.py @@ -1,6 +1,7 @@ from .core.constants import DIAGNOSTIC_KINDS from .core.diagnostics_storage import is_severity_included from .core.diagnostics_storage import ParsedUri +from .core.input_handlers import PreselectedListInputHandler from .core.paths import project_base_dir from .core.paths import project_path from .core.paths import simple_project_path @@ -12,7 +13,7 @@ from .core.sessions import Session from .core.settings import userprefs from .core.types import ClientConfig -from .core.typing import Any, Dict, Iterator, List, Optional, Tuple, Union +from .core.typing import Dict, Iterator, List, Optional, Tuple, Union from .core.typing import cast from .core.url import parse_uri, unparse_uri from .core.views import diagnostic_severity @@ -23,8 +24,6 @@ from .core.views import MissingUriError from .core.views import to_encoded_filename from .core.views import uri_from_view -from abc import ABCMeta -from abc import abstractmethod from collections import Counter, OrderedDict from pathlib import Path import functools @@ -94,46 +93,6 @@ def input_description(self) -> str: return "Goto Diagnostic" -ListItemsReturn = Union[List[str], Tuple[List[str], int], List[Tuple[str, Any]], Tuple[List[Tuple[str, Any]], int], - List[sublime.ListInputItem], Tuple[List[sublime.ListInputItem], int]] - - -class PreselectedListInputHandler(sublime_plugin.ListInputHandler, metaclass=ABCMeta): - """ - Similar to ListInputHandler, but allows to preselect a value like some of the input overlays in Sublime Merge. - Inspired by https://github.com/sublimehq/sublime_text/issues/5507. - - Subclasses of PreselectedListInputHandler must not implement the `list_items` method, but instead `get_list_items`, - i.e. just prepend `get_` to the regular `list_items` method. - - When an instance of PreselectedListInputHandler is created, it must be given the window as an argument. - An optional second argument `initial_value` can be provided to preselect a value. - """ - - def __init__( - self, window: sublime.Window, initial_value: Optional[Union[str, sublime.ListInputItem]] = None - ) -> None: - super().__init__() - self._window = window - self._initial_value = initial_value - - def list_items(self) -> ListItemsReturn: - if self._initial_value is not None: - sublime.set_timeout(self._select_and_reset) - return [self._initial_value], 0 # pyright: ignore[reportGeneralTypeIssues] - else: - return self.get_list_items() - - def _select_and_reset(self) -> None: - self._initial_value = None - if self._window.is_valid(): - self._window.run_command('select') - - @abstractmethod - def get_list_items(self) -> ListItemsReturn: - raise NotImplementedError() - - class DiagnosticUriInputHandler(PreselectedListInputHandler): _preview = None # type: Optional[sublime.View] uri = None # Optional[DocumentUri] diff --git a/plugin/symbols.py b/plugin/symbols.py index ad6186d8c..deec5c4c4 100644 --- a/plugin/symbols.py +++ b/plugin/symbols.py @@ -1,20 +1,26 @@ -import weakref -from .core.constants import SublimeKind from .core.constants import SYMBOL_KINDS +from .core.input_handlers import DynamicListInputHandler +from .core.input_handlers import PreselectedListInputHandler +from .core.promise import Promise from .core.protocol import DocumentSymbol from .core.protocol import DocumentSymbolParams +from .core.protocol import Location from .core.protocol import Point +from .core.protocol import Range from .core.protocol import Request from .core.protocol import SymbolInformation from .core.protocol import SymbolKind from .core.protocol import SymbolTag +from .core.protocol import WorkspaceSymbol from .core.registry import LspTextCommand +from .core.registry import LspWindowCommand from .core.sessions import print_to_status_bar -from .core.typing import Any, List, Optional, Tuple, Dict, Union, cast +from .core.typing import Any, Dict, List, NotRequired, Optional, Tuple, TypedDict, TypeGuard, Union +from .core.typing import cast from .core.views import offset_to_point from .core.views import range_to_region from .core.views import text_document_identifier -from .goto_diagnostic import PreselectedListInputHandler +import functools import os import sublime import sublime_plugin @@ -52,58 +58,69 @@ } # type: Dict[SymbolKind, str] -def unpack_lsp_kind(kind: SymbolKind) -> SublimeKind: - return SYMBOL_KINDS.get(kind, sublime.KIND_AMBIGUOUS) - - -def symbol_information_to_quick_panel_item( - item: SymbolInformation, - show_file_name: bool = True -) -> sublime.QuickPanelItem: - st_kind, st_icon, st_display_type = unpack_lsp_kind(item['kind']) - tags = item.get("tags") or [] - if SymbolTag.Deprecated in tags: - st_display_type = "⚠ {} - Deprecated".format(st_display_type) - container = item.get("containerName") or "" - details = [] # List[str] - if container: - details.append(container) - if show_file_name: - file_name = os.path.basename(item['location']['uri']) - details.append(file_name) - return sublime.QuickPanelItem( - trigger=item["name"], - details=details, - annotation=st_display_type, - kind=(st_kind, st_icon, st_display_type)) +DocumentSymbolValue = TypedDict('DocumentSymbolValue', { + 'deprecated': bool, + 'kind': int, + 'range': Range +}) + +WorkspaceSymbolValue = TypedDict('WorkspaceSymbolValue', { + 'deprecated': bool, + 'kind': int, + 'location': NotRequired[Location], + 'session': str, + 'workspaceSymbol': NotRequired[WorkspaceSymbol] +}) + + +def is_document_symbol_value(val: Any) -> TypeGuard[DocumentSymbolValue]: + return isinstance(val, dict) and all(key in val for key in ('deprecated', 'kind', 'range')) def symbol_to_list_input_item( - item: Union[DocumentSymbol, SymbolInformation], hierarchy: str = '' + item: Union[DocumentSymbol, WorkspaceSymbol, SymbolInformation], + hierarchy: str = '', + session_name: Optional[str] = None ) -> sublime.ListInputItem: name = item['name'] kind = item['kind'] st_kind = SYMBOL_KINDS.get(kind, sublime.KIND_AMBIGUOUS) - details = [] + details = [] # type: List[str] + deprecated = SymbolTag.Deprecated in (item.get('tags') or []) or item.get('deprecated', False) + value = {'kind': kind, 'deprecated': deprecated} + details_separator = " • " selection_range = item.get('selectionRange') - if selection_range: + if selection_range: # Response from textDocument/documentSymbol request item = cast(DocumentSymbol, item) detail = item.get('detail') if detail: details.append(detail) if hierarchy: details.append(hierarchy + " > " + name) - else: + value['range'] = selection_range + elif session_name is None: # Response from textDocument/documentSymbol request item = cast(SymbolInformation, item) container_name = item.get('containerName') if container_name: details.append(container_name) - selection_range = item['location']['range'] - deprecated = SymbolTag.Deprecated in (item.get('tags') or []) or item.get('deprecated', False) + value['range'] = item['location']['range'] + else: # Response from workspace/symbol request + item = cast(WorkspaceSymbol, item) # Either WorkspaceSymbol or SymbolInformation, but possibly undecidable + details_separator = " > " + location = item['location'] + details.append(os.path.basename(location['uri'])) + container_name = item.get('containerName') + if container_name: + details.append(container_name) + if 'range' in location: + value['location'] = location + else: + value['workspaceSymbol'] = item + value['session'] = session_name return sublime.ListInputItem( name, - {'kind': kind, 'range': selection_range, 'deprecated': deprecated}, - details=" • ".join(details), + value, + details=details_separator.join(details), annotation=st_kind[2], kind=st_kind ) @@ -204,7 +221,7 @@ def handle_response_async(self, response: Union[List[DocumentSymbol], List[Symbo window = self.view.window() if window: self.cached = True - window.run_command('show_overlay', {'overlay': 'command_palette', 'command': 'lsp_document_symbols'}) + window.run_command('show_overlay', {'overlay': 'command_palette', 'command': self.name()}) def handle_response_error(self, error: Any) -> None: self._reset_suppress_input() @@ -296,14 +313,12 @@ def list_items(self) -> Tuple[List[sublime.ListInputItem], int]: break return items, selected_index - def preview(self, text: Any) -> Union[str, sublime.Html, None]: - if isinstance(text, dict): - r = text.get('range') - if r: - region = range_to_region(r, self.view) - self.view.run_command('lsp_selection_set', {'regions': [(region.a, region.b)]}) - self.view.show_at_center(region.a) - if text.get('deprecated'): + def preview(self, text: Optional[DocumentSymbolValue]) -> Union[str, sublime.Html, None]: + if is_document_symbol_value(text): + region = range_to_region(text['range'], self.view) + self.view.run_command('lsp_selection_set', {'regions': [(region.a, region.b)]}) + self.view.show_at_center(region.a) + if text['deprecated']: return "⚠ Deprecated" return "" @@ -313,48 +328,67 @@ def cancel(self) -> None: self.view.show_at_center(self.old_selection[0].begin()) -class SymbolQueryInput(sublime_plugin.TextInputHandler): - def want_event(self) -> bool: - return False +class LspWorkspaceSymbolsCommand(LspWindowCommand): - def placeholder(self) -> str: - return "Enter symbol name" + capability = 'workspaceSymbolProvider' + + def run(self, symbol: WorkspaceSymbolValue) -> None: + session_name = symbol['session'] + session = self.session_by_name(session_name) + if session: + location = symbol.get('location') + if location: + session.open_location_async(location, sublime.ENCODED_POSITION) + else: + session.send_request( + Request.resolveWorkspaceSymbol(symbol['workspaceSymbol']), # type: ignore + functools.partial(self._on_resolved_symbol_async, session_name)) + def input(self, args: Dict[str, Any]) -> Optional[sublime_plugin.ListInputHandler]: + if 'symbol' not in args: + return WorkspaceSymbolsInputHandler(self, args) + return None -class LspWorkspaceSymbolsCommand(LspTextCommand): + def _on_resolved_symbol_async(self, session_name: str, response: WorkspaceSymbol) -> None: + location = cast(Location, response['location']) + session = self.session_by_name(session_name) + if session: + session.open_location_async(location, sublime.ENCODED_POSITION) - capability = 'workspaceSymbolProvider' - def input(self, _args: Any) -> sublime_plugin.TextInputHandler: - return SymbolQueryInput() +class WorkspaceSymbolsInputHandler(DynamicListInputHandler): - def run(self, edit: sublime.Edit, symbol_query_input: str, event: Optional[Any] = None) -> None: - session = self.best_session(self.capability) - if session: - self.weaksession = weakref.ref(session) - session.send_request( - Request.workspaceSymbol({"query": symbol_query_input}), - lambda r: self._handle_response(symbol_query_input, r), - self._handle_error) - - def _open_file(self, symbols: List[SymbolInformation], index: int) -> None: - if index != -1: - session = self.weaksession() - if session: - session.open_location_async(symbols[index]['location'], sublime.ENCODED_POSITION) + def name(self) -> str: + return 'symbol' - def _handle_response(self, query: str, response: Union[List[SymbolInformation], None]) -> None: - if response: - matches = response - window = self.view.window() - if window: - window.show_quick_panel( - list(map(symbol_information_to_quick_panel_item, matches)), - lambda i: self._open_file(matches, i)) - else: - sublime.message_dialog("No matches found for query: '{}'".format(query)) + def placeholder(self) -> str: + return "Start typing to search" - def _handle_error(self, error: Dict[str, Any]) -> None: - reason = error.get("message", "none provided by server :(") - msg = "command 'workspace/symbol' failed. Reason: {}".format(reason) - sublime.error_message(msg) + def preview(self, text: Optional[WorkspaceSymbolValue]) -> Union[str, sublime.Html, None]: + if isinstance(text, dict) and text.get('deprecated'): + return "⚠ Deprecated" + return "" + + def on_modified(self, text: str) -> None: + if not self.input_view: + return + change_count = self.input_view.change_count() + self.command = cast(LspWindowCommand, self.command) + promises = [] # type: List[Promise[List[sublime.ListInputItem]]] + for session in self.command.sessions(): + promises.append( + session.send_request_task(Request.workspaceSymbol({"query": text})) + .then(functools.partial(self._handle_response_async, session.config.name))) + Promise.all(promises).then(functools.partial(self._on_all_responses, change_count)) + + def _handle_response_async( + self, session_name: str, response: Union[List[SymbolInformation], List[WorkspaceSymbol], None] + ) -> List[sublime.ListInputItem]: + return [symbol_to_list_input_item(item, session_name=session_name) for item in response] if response else [] + + def _on_all_responses(self, change_count: int, item_lists: List[List[sublime.ListInputItem]]) -> None: + if self.input_view and self.input_view.change_count() == change_count: + items = [] # type: List[sublime.ListInputItem] + for item_list in item_lists: + items.extend(item_list) + self.update(items) diff --git a/tests/test_document_symbols.py b/tests/test_document_symbols.py deleted file mode 100644 index b86ba2572..000000000 --- a/tests/test_document_symbols.py +++ /dev/null @@ -1,24 +0,0 @@ -from setup import TextDocumentTestCase -from LSP.plugin.core.typing import Generator -from LSP.plugin.symbols import symbol_information_to_quick_panel_item -from LSP.plugin.core.protocol import SymbolTag - - -class DocumentSymbolTests(TextDocumentTestCase): - def test_show_deprecated_flag_for_symbol_information(self) -> 'Generator': - symbol_information = { - "name": 'Name', - "kind": 6, # Method - "tags": [SymbolTag.Deprecated], - } - formatted_symbol_information = symbol_information_to_quick_panel_item(symbol_information, show_file_name=False) - self.assertEqual('⚠ Method - Deprecated', formatted_symbol_information.annotation) - - def test_dont_show_deprecated_flag_for_symbol_information(self) -> 'Generator': - symbol_information = { - "name": 'Name', - "kind": 6, # Method - # to deprecated tags - } - formatted_symbol_information = symbol_information_to_quick_panel_item(symbol_information, show_file_name=False) - self.assertEqual('Method', formatted_symbol_information.annotation)