# 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)