Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hierarchical doc symbols #599

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
83 changes: 82 additions & 1 deletion pyls/plugins/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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,
Expand Down
30 changes: 26 additions & 4 deletions test/plugins/test_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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__'