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

Introduce hierarchical documentSymbol support #537

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
74 changes: 64 additions & 10 deletions pyls/plugins/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
50 changes: 48 additions & 2 deletions test/plugins/test_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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