diff --git a/appveyor.yml b/appveyor.yml index 530e800d..17ead684 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,11 @@ environment: matrix: - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7.15" + PYTHON_VERSION: "2.7.16" PYTHON_ARCH: "64" - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.9" + PYTHON_VERSION: "3.4.10" PYTHON_ARCH: "64" matrix: diff --git a/pyls/plugins/symbols.py b/pyls/plugins/symbols.py index ced97218..68c43303 100644 --- a/pyls/plugins/symbols.py +++ b/pyls/plugins/symbols.py @@ -9,7 +9,57 @@ @hookimpl def pyls_document_symbols(config, document): all_scopes = config.plugin_settings('jedi_symbols').get('all_scopes', True) - definitions = document.jedi_names(all_scopes=all_scopes) + + symbols_capabilities = config.capabilities.get('textDocument', {}).get('documentSymbol', {}) + + if symbols_capabilities.get('hierarchicalDocumentSymbolSupport', False): + # Disable all_scopes so we can do our own symbol recursion + log.debug("Returning hierarchical document symbols for %s", document) + return _hierarchical_symbols(document.jedi_names(all_scopes=False)) + else: + return _flat_symbols(document, document.jedi_names(all_scopes=all_scopes)) + + +def _hierarchical_symbols(definitions): + """Return symbols as recursive DocumentSymbol objects.""" + defs = [] + for definition in definitions: + if not _include_def(definition): + continue + + symbol = _document_symbol(definition) + if symbol is not None: + defs.append(symbol) + + return defs + + +def _document_symbol(definition): + if not _include_def(definition): + return None + + if definition.type == 'statement': + # Currently these seem to cause Jedi to error when calling defined_names() + children = [] + else: + try: + children = _hierarchical_symbols(definition.defined_names()) + except Exception: # pylint: disable=broad-except + log.exception("Failed to get children of %s symbol: %s", definition.type, definition) + children = [] + + return { + 'name': definition.name, + 'detail': _detail_name(definition), + 'range': _range(definition), + 'selectionRange': _name_range(definition), + 'kind': _kind(definition), + 'children': children + } + + +def _flat_symbols(document, definitions): + """Return symbols as SymbolInformation object.""" return [{ 'name': d.name, 'containerName': _container(d), @@ -23,14 +73,26 @@ def pyls_document_symbols(config, document): def _include_def(definition): return ( + # Skip built-ins + not definition.in_builtin_module() and # Don't tend to include parameters as symbols definition.type != 'param' and # Unused vars should also be skipped definition.name != '_' and + # Skip imports, since they're not _really_ defined by us + not definition._name.is_import() and + # Only definitions for which we know the "kind" _kind(definition) is not None ) +def _detail_name(definition): + name = definition.full_name + if name.startswith('__main__.'): + name = name[len('__main__.'):] + return name + + def _container(definition): try: # Jedi sometimes fails here. @@ -46,6 +108,10 @@ def _container(definition): def _range(definition): + """Return the LSP range for the symbol. + + For a function, this would be all lines of code for the function. + """ # This gets us more accurate end position definition = definition._name.tree_name.get_definition() (start_line, start_column) = definition.start_pos @@ -56,6 +122,21 @@ def _range(definition): } +def _name_range(definition): + """Returns the LSP range for the name of the symbol. + + For a function, this would only be the range of the function name. + """ + # This gets us more accurate end position + 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, diff --git a/test/plugins/test_symbols.py b/test/plugins/test_symbols.py index b4bb8b0d..ea3c8bf8 100644 --- a/test/plugins/test_symbols.py +++ b/test/plugins/test_symbols.py @@ -26,8 +26,8 @@ def test_symbols(config): config.update({'plugins': {'jedi_symbols': {'all_scopes': False}}}) symbols = pyls_document_symbols(config, doc) - # All four symbols (import sys, a, B, main, y) - assert len(symbols) == 5 + # All four symbols (a, B, main, y) + assert len(symbols) == 4 def sym(name): return [s for s in symbols if s['name'] == name][0] @@ -49,8 +49,8 @@ def test_symbols_all_scopes(config): doc = Document(DOC_URI, DOC) symbols = pyls_document_symbols(config, doc) - # All eight symbols (import sys, a, B, __init__, x, y, main, y) - assert len(symbols) == 8 + # All eight symbols (a, B, __init__, x, y, main, y) + assert len(symbols) == 7 def sym(name): return [s for s in symbols if s['name'] == name][0] @@ -63,3 +63,25 @@ 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): + # Enable client support + config.capabilities['textDocument'] = {'documentSymbol': {'hierarchicalDocumentSymbolSupport': True}} + + doc = Document(DOC_URI, DOC) + symbols = pyls_document_symbols(config, doc) + + # All four symbols (a, B, main, y) + assert len(symbols) == 4 + + # Ensure a has no children + sym_a = symbols[0] + assert sym_a['name'] == 'a' + assert not sym_a['children'] + + # Ensure B has a single __init__ function child + sym_b = symbols[1] + assert sym_b['name'] == 'B' + assert len(sym_b['children']) == 1 + assert sym_b['children'][0]['name'] == '__init__'