diff --git a/pyls/plugins/symbols.py b/pyls/plugins/symbols.py index ced97218..4c3b3b37 100644 --- a/pyls/plugins/symbols.py +++ b/pyls/plugins/symbols.py @@ -8,7 +8,41 @@ @hookimpl def pyls_document_symbols(config, document): + useHierarchicalSymbols = config.plugin_settings('jedi_symbols').get('hierarchical_symbols', None) + if useHierarchicalSymbols is None: + useHierarchicalSymbols = (config.capabilities + .get("textDocument", {}) + .get("documentSymbol", {}) + .get("hierarchicalDocumentSymbolSupport", False)) + if not useHierarchicalSymbols: + return pyls_document_symbols_legacy(config, document) + # returns DocumentSymbol[] + hide_imports = config.plugin_settings('jedi_symbols').get('hide_imports', False) + definitions = document.jedi_names(all_scopes=False) + + def transform(d): + include_d = _include_def(d, hide_imports) + if include_d is None: + return None + children = [dt for dt in (transform(d1) for d1 in d.defined_names()) if dt] if include_d else None + detailName = d.full_name + if detailName and detailName.startswith("__main__."): + detailName = detailName[9:] + return { + 'name': d.name, + 'detail': detailName, + 'range': _range(d), + 'selectionRange': _name_range(d), + 'kind': _kind(d), + 'children': children + } + return [dt for dt in (transform(d) for d in definitions) if dt] + + +def pyls_document_symbols_legacy(config, document): + # returns SymbolInformation[] all_scopes = config.plugin_settings('jedi_symbols').get('all_scopes', True) + hide_imports = config.plugin_settings('jedi_symbols').get('hide_imports', False) definitions = document.jedi_names(all_scopes=all_scopes) return [{ 'name': d.name, @@ -18,17 +52,25 @@ def pyls_document_symbols(config, document): 'range': _range(d), }, 'kind': _kind(d), - } for d in definitions if _include_def(d)] + } for d in definitions if _include_def(d, hide_imports) is not None] -def _include_def(definition): - return ( - # Don't tend to include parameters as symbols - definition.type != 'param' and - # Unused vars should also be skipped - definition.name != '_' and - _kind(definition) is not None - ) +def _include_def(definition, hide_imports=True): + # returns + # True: include def and sub-defs + # False: include def but do not include sub-defs + # None: Do not include def or sub-defs + if ( # Unused vars should also be skipped + definition.name != '_' and + definition.is_definition() and + not definition.in_builtin_module() and + _kind(definition) is not None + ): + if definition._name.is_import(): + return None if hide_imports else False + # for `statement`, we do not enumerate its child nodes. It tends to cause Error. + return definition.type not in ("statement",) + return None def _container(definition): @@ -56,6 +98,17 @@ def _range(definition): } +def _name_range(definition): + # Gets the range of symbol name (e.g. function name of a function) + definition = definition._name.tree_name + (start_line, start_column) = definition.start_pos + (end_line, end_column) = definition.end_pos + return { + 'start': {'line': start_line - 1, 'character': start_column}, + 'end': {'line': end_line - 1, 'character': end_column} + } + + _SYMBOL_KIND_MAP = { 'none': SymbolKind.Variable, 'type': SymbolKind.Class, @@ -85,7 +138,8 @@ def _range(definition): 'constant': SymbolKind.Constant, 'variable': SymbolKind.Variable, 'value': SymbolKind.Variable, - 'param': SymbolKind.Variable, + # Don't tend to include parameters as symbols + # 'param': SymbolKind.Variable, 'statement': SymbolKind.Variable, 'boolean': SymbolKind.Boolean, 'int': SymbolKind.Number, diff --git a/test/plugins/test_symbols.py b/test/plugins/test_symbols.py index b4bb8b0d..be294c2c 100644 --- a/test/plugins/test_symbols.py +++ b/test/plugins/test_symbols.py @@ -23,10 +23,16 @@ def main(x): def test_symbols(config): doc = Document(DOC_URI, DOC) - config.update({'plugins': {'jedi_symbols': {'all_scopes': False}}}) + config.update({'plugins': {'jedi_symbols': {'all_scopes': False, 'hide_imports': True}}}) symbols = pyls_document_symbols(config, doc) - # All four symbols (import sys, a, B, main, y) + # Only local symbols (a, B, main, y) + assert len(symbols) == 4 + + config.update({'plugins': {'jedi_symbols': {'all_scopes': False, 'hide_imports': False}}}) + symbols = pyls_document_symbols(config, doc) + + # All five symbols (import sys, a, B, main, y) assert len(symbols) == 5 def sym(name): @@ -47,6 +53,14 @@ def sym(name): def test_symbols_all_scopes(config): doc = Document(DOC_URI, DOC) + + config.update({'plugins': {'jedi_symbols': {'all_scopes': True, 'hide_imports': True}}}) + symbols = pyls_document_symbols(config, doc) + + # Only local symbols (a, B, __init__, x, y, main, y) + assert len(symbols) == 7 + + config.update({'plugins': {'jedi_symbols': {'all_scopes': True, 'hide_imports': False}}}) symbols = pyls_document_symbols(config, doc) # All eight symbols (import sys, a, B, __init__, x, y, main, y) @@ -63,3 +77,35 @@ def sym(name): # Not going to get too in-depth here else we're just testing Jedi assert sym('a')['location']['range']['start'] == {'line': 2, 'character': 0} + + +def test_symbols_hierarchical(config): + doc = Document(DOC_URI, DOC) + + config.update({'plugins': {'jedi_symbols': {'hierarchical_symbols': True, 'hide_imports': True}}}) + symbols = pyls_document_symbols(config, doc) + + # Only local symbols (a, B, main, y) + assert len(symbols) == 4 + + config.update({'plugins': {'jedi_symbols': {'hierarchical_symbols': True, 'hide_imports': False}}}) + symbols = pyls_document_symbols(config, doc) + + # All five symbols (import sys, a, B, main, y) + assert len(symbols) == 5 + + def sym(name): + return [s for s in symbols if s['name'] == name][0] + + def child_sym(sym, name): + if not sym['children']: + return None + return [s for s in sym['children'] if s['name'] == name][0] + + # Check we have some sane mappings to VSCode constants + assert sym('a')['kind'] == SymbolKind.Variable + assert sym('B')['kind'] == SymbolKind.Class + assert len(sym('B')['children']) == 1 + assert child_sym(sym('B'), '__init__')['kind'] == SymbolKind.Function + assert child_sym(sym('B'), '__init__')['detail'] == 'B.__init__' + assert sym('main')['kind'] == SymbolKind.Function