219 lines
7.9 KiB
Python
219 lines
7.9 KiB
Python
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
# Copyright 2021- Python Language Server Contributors.
|
|
|
|
import logging
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from pylsp import hookimpl
|
|
from pylsp.lsp import SymbolKind
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@hookimpl
|
|
def pylsp_document_symbols(config, document):
|
|
symbols_settings = config.plugin_settings("jedi_symbols")
|
|
all_scopes = symbols_settings.get("all_scopes", True)
|
|
add_import_symbols = symbols_settings.get("include_import_symbols", True)
|
|
definitions = document.jedi_names(all_scopes=all_scopes)
|
|
symbols = []
|
|
exclude = set({})
|
|
redefinitions = {}
|
|
pattern_import = re.compile(
|
|
r"^\s*(?!#)\s*(from\s+[.\w]+(\.[\w]+)*\s+import\s+[\w\s,()*]+|import\s+[\w\s,.*]+)"
|
|
)
|
|
|
|
while definitions != []:
|
|
d = definitions.pop(0)
|
|
|
|
# Skip symbols imported from other modules.
|
|
if not add_import_symbols:
|
|
# Skip if there's an import in the code the symbol is defined.
|
|
code = d.get_line_code()
|
|
|
|
if pattern_import.match(code):
|
|
continue
|
|
|
|
# Skip imported symbols comparing module names.
|
|
sym_full_name = d.full_name
|
|
if sym_full_name is not None:
|
|
document_dot_path = document.dot_path
|
|
|
|
# We assume a symbol is imported from another module to start
|
|
# with.
|
|
imported_symbol = True
|
|
|
|
# The last element of sym_full_name is the symbol itself, so
|
|
# we need to discard it to do module comparisons below.
|
|
if "." in sym_full_name:
|
|
sym_module_name = sym_full_name.rpartition(".")[0]
|
|
else:
|
|
sym_module_name = sym_full_name
|
|
|
|
# This is necessary to display symbols in init files (the checks
|
|
# below fail without it).
|
|
if document_dot_path.endswith("__init__"):
|
|
document_dot_path = document_dot_path.rpartition(".")[0]
|
|
|
|
# document_dot_path is the module where the symbol is imported,
|
|
# whereas sym_module_name is the one where it was declared.
|
|
if document_dot_path in sym_module_name:
|
|
# If document_dot_path is in sym_module_name, we can safely assume
|
|
# that the symbol was declared in the document.
|
|
imported_symbol = False
|
|
elif sym_module_name.split(".")[0] in document_dot_path.split("."):
|
|
# If the first module in sym_module_name is one of the modules in
|
|
# document_dot_path, we need to check if sym_module_name starts
|
|
# with the modules in document_dot_path.
|
|
document_mods = document_dot_path.split(".")
|
|
for i in range(1, len(document_mods) + 1):
|
|
submod = ".".join(document_mods[-i:])
|
|
if sym_module_name.startswith(submod):
|
|
imported_symbol = False
|
|
break
|
|
|
|
# When there's no __init__.py next to a file or in one of its
|
|
# parents, the checks above fail. However, Jedi has a nice way
|
|
# to tell if the symbol was declared in the same file: if
|
|
# sym_module_name starts by __main__.
|
|
if imported_symbol:
|
|
if not sym_module_name.startswith("__main__"):
|
|
continue
|
|
else:
|
|
# We need to skip symbols if their definition doesn't have `full_name` info, they
|
|
# are detected as a definition, but their description (e.g. `class Foo`) doesn't
|
|
# match the code where they're detected by Jedi. This happens for relative imports.
|
|
if _include_def(d):
|
|
if d.description not in d.get_line_code():
|
|
continue
|
|
else:
|
|
continue
|
|
|
|
if _include_def(d) and Path(document.path) == Path(d.module_path):
|
|
tuple_range = _tuple_range(d)
|
|
if tuple_range in exclude:
|
|
continue
|
|
|
|
kind = redefinitions.get(tuple_range, None)
|
|
if kind is not None:
|
|
exclude |= {tuple_range}
|
|
|
|
if d.type == "statement":
|
|
if d.description.startswith("self"):
|
|
kind = "field"
|
|
|
|
symbol = {
|
|
"name": d.name,
|
|
"containerName": _container(d),
|
|
"location": {
|
|
"uri": document.uri,
|
|
"range": _range(d),
|
|
},
|
|
"kind": _kind(d) if kind is None else _SYMBOL_KIND_MAP[kind],
|
|
}
|
|
symbols.append(symbol)
|
|
|
|
if d.type == "class":
|
|
try:
|
|
defined_names = list(d.defined_names())
|
|
for method in defined_names:
|
|
if method.type == "function":
|
|
redefinitions[_tuple_range(method)] = "method"
|
|
elif method.type == "statement":
|
|
redefinitions[_tuple_range(method)] = "field"
|
|
else:
|
|
redefinitions[_tuple_range(method)] = method.type
|
|
definitions = list(defined_names) + definitions
|
|
except Exception:
|
|
pass
|
|
return symbols
|
|
|
|
|
|
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 _container(definition):
|
|
try:
|
|
# Jedi sometimes fails here.
|
|
parent = definition.parent()
|
|
# Here we check that a grand-parent exists to avoid declaring symbols
|
|
# as children of the module.
|
|
if parent.parent():
|
|
return parent.name
|
|
except:
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
def _range(definition):
|
|
# This gets us more accurate end position
|
|
definition = definition._name.tree_name.get_definition()
|
|
(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},
|
|
}
|
|
|
|
|
|
def _tuple_range(definition):
|
|
definition = definition._name.tree_name.get_definition()
|
|
return (definition.start_pos, definition.end_pos)
|
|
|
|
|
|
_SYMBOL_KIND_MAP = {
|
|
"none": SymbolKind.Variable,
|
|
"type": SymbolKind.Class,
|
|
"tuple": SymbolKind.Class,
|
|
"dict": SymbolKind.Class,
|
|
"dictionary": SymbolKind.Class,
|
|
"function": SymbolKind.Function,
|
|
"lambda": SymbolKind.Function,
|
|
"generator": SymbolKind.Function,
|
|
"class": SymbolKind.Class,
|
|
"instance": SymbolKind.Class,
|
|
"method": SymbolKind.Method,
|
|
"builtin": SymbolKind.Class,
|
|
"builtinfunction": SymbolKind.Function,
|
|
"module": SymbolKind.Module,
|
|
"file": SymbolKind.File,
|
|
"xrange": SymbolKind.Array,
|
|
"slice": SymbolKind.Class,
|
|
"traceback": SymbolKind.Class,
|
|
"frame": SymbolKind.Class,
|
|
"buffer": SymbolKind.Array,
|
|
"dictproxy": SymbolKind.Class,
|
|
"funcdef": SymbolKind.Function,
|
|
"property": SymbolKind.Property,
|
|
"import": SymbolKind.Module,
|
|
"keyword": SymbolKind.Variable,
|
|
"constant": SymbolKind.Constant,
|
|
"variable": SymbolKind.Variable,
|
|
"value": SymbolKind.Variable,
|
|
"param": SymbolKind.Variable,
|
|
"statement": SymbolKind.Variable,
|
|
"boolean": SymbolKind.Boolean,
|
|
"int": SymbolKind.Number,
|
|
"longlean": SymbolKind.Number,
|
|
"float": SymbolKind.Number,
|
|
"complex": SymbolKind.Number,
|
|
"string": SymbolKind.String,
|
|
"unicode": SymbolKind.String,
|
|
"list": SymbolKind.Array,
|
|
"field": SymbolKind.Field,
|
|
}
|
|
|
|
|
|
def _kind(d):
|
|
"""Return the VSCode Symbol Type"""
|
|
return _SYMBOL_KIND_MAP.get(d.type)
|