# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. import logging import os import parso from pylsp import _utils, hookimpl, lsp from pylsp.plugins._resolvers import LABEL_RESOLVER, SNIPPET_RESOLVER log = logging.getLogger(__name__) # Map to the LSP type # > Valid values for type are ``module``, `` class ``, ``instance``, ``function``, # > ``param``, ``path``, ``keyword``, ``property`` and ``statement``. # see: https://jedi.readthedocs.io/en/latest/docs/api-classes.html#jedi.api.classes.BaseName.type _TYPE_MAP = { "module": lsp.CompletionItemKind.Module, "namespace": lsp.CompletionItemKind.Module, # to be added in Jedi 0.18+ "class": lsp.CompletionItemKind.Class, "instance": lsp.CompletionItemKind.Reference, "function": lsp.CompletionItemKind.Function, "param": lsp.CompletionItemKind.Variable, "path": lsp.CompletionItemKind.File, "keyword": lsp.CompletionItemKind.Keyword, "property": lsp.CompletionItemKind.Property, # added in Jedi 0.18 "statement": lsp.CompletionItemKind.Variable, } # Types of parso nodes for which snippet is not included in the completion _IMPORTS = ("import_name", "import_from") # Types of parso node for errors _ERRORS = ("error_node",) @hookimpl def pylsp_completions(config, document, position): """Get formatted completions for current code position""" settings = config.plugin_settings("jedi_completion", document_path=document.path) resolve_eagerly = settings.get("eager", False) signature_config = config.settings().get("signature", {}) code_position = _utils.position_to_jedi_linecolumn(document, position) code_position["fuzzy"] = settings.get("fuzzy", False) completions = document.jedi_script(use_document_path=True).complete(**code_position) if not completions: return None completion_capabilities = config.capabilities.get("textDocument", {}).get( "completion", {} ) item_capabilities = completion_capabilities.get("completionItem", {}) snippet_support = item_capabilities.get("snippetSupport") supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) should_include_params = settings.get("include_params") should_include_class_objects = settings.get("include_class_objects", False) should_include_function_objects = settings.get("include_function_objects", False) max_to_resolve = settings.get("resolve_at_most", 25) modules_to_cache_for = settings.get("cache_for", None) if modules_to_cache_for is not None: LABEL_RESOLVER.cached_modules = modules_to_cache_for SNIPPET_RESOLVER.cached_modules = modules_to_cache_for include_params = ( snippet_support and should_include_params and use_snippets(document, position) ) include_class_objects = ( snippet_support and should_include_class_objects and use_snippets(document, position) ) include_function_objects = ( snippet_support and should_include_function_objects and use_snippets(document, position) ) ready_completions = [ _format_completion( c, markup_kind=preferred_markup_kind, include_params=include_params if c.type in ["class", "function"] else False, resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve), snippet_support=snippet_support, signature_config=signature_config, ) for i, c in enumerate(completions) ] # TODO split up once other improvements are merged if include_class_objects: for i, c in enumerate(completions): if c.type == "class": completion_dict = _format_completion( c, markup_kind=preferred_markup_kind, include_params=False, resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve), snippet_support=snippet_support, signature_config=signature_config, ) completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter completion_dict["label"] += " object" ready_completions.append(completion_dict) if include_function_objects: for i, c in enumerate(completions): if c.type == "function": completion_dict = _format_completion( c, markup_kind=preferred_markup_kind, include_params=False, resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve), snippet_support=snippet_support, signature_config=signature_config, ) completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter completion_dict["label"] += " object" ready_completions.append(completion_dict) for completion_dict in ready_completions: completion_dict["data"] = {"doc_uri": document.uri} # most recently retrieved completion items, used for resolution document.shared_data["LAST_JEDI_COMPLETIONS"] = { # label is the only required property; here it is assumed to be unique completion["label"]: (completion, data) for completion, data in zip(ready_completions, completions) } return ready_completions or None @hookimpl def pylsp_completion_item_resolve( config, completion_item, document, ): """Resolve formatted completion for given non-resolved completion""" shared_data = document.shared_data["LAST_JEDI_COMPLETIONS"].get( completion_item["label"] ) completion_capabilities = config.capabilities.get("textDocument", {}).get( "completion", {} ) item_capabilities = completion_capabilities.get("completionItem", {}) supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) if shared_data: completion, data = shared_data return _resolve_completion( completion, data, markup_kind=preferred_markup_kind, signature_config=config.settings().get("signature", {}), ) return completion_item def is_exception_class(name): """ Determine if a class name is an instance of an Exception. This returns `False` if the name given corresponds with a instance of the 'Exception' class, `True` otherwise """ try: return name in [cls.__name__ for cls in Exception.__subclasses__()] except AttributeError: # Needed in case a class don't uses new-style # class definition in Python 2 return False def use_snippets(document, position): """ Determine if it's necessary to return snippets in code completions. This returns `False` if a completion is being requested on an import statement, `True` otherwise. """ line = position["line"] lines = document.source.split("\n", line) act_lines = [lines[line][: position["character"]]] line -= 1 last_character = "" while line > -1: act_line = lines[line] if ( act_line.rstrip().endswith("\\") or act_line.rstrip().endswith("(") or act_line.rstrip().endswith(",") ): act_lines.insert(0, act_line) line -= 1 if act_line.rstrip().endswith("("): # Needs to be added to the end of the code before parsing # to make it valid, otherwise the node type could end # being an 'error_node' for multi-line imports that use '(' last_character = ")" else: break if "(" in act_lines[-1].strip(): last_character = ")" code = "\n".join(act_lines).rsplit(";", maxsplit=1)[-1].strip() + last_character tokens = parso.parse(code) expr_type = tokens.children[0].type return expr_type not in _IMPORTS and not (expr_type in _ERRORS and "import" in code) def _resolve_completion(completion, d, markup_kind: str, signature_config: dict): completion["detail"] = _detail(d) try: docs = _utils.format_docstring( d.docstring(raw=True), signatures=[signature.to_string() for signature in d.get_signatures()], markup_kind=markup_kind, signature_config=signature_config, ) except Exception: docs = "" completion["documentation"] = docs return completion def _format_completion( d, markup_kind: str, include_params=True, resolve=False, resolve_label_or_snippet=False, snippet_support=False, signature_config=None, ): completion = { "label": _label(d, resolve_label_or_snippet), "kind": _TYPE_MAP.get(d.type), "sortText": _sort_text(d), "insertText": d.name, } if resolve: completion = _resolve_completion( completion, d, markup_kind, signature_config=signature_config ) # Adjustments for file completions if d.type == "path": path = os.path.normpath(d.name) # If the completion ends with os.sep, it means it's a directory. So we add os.sep at the end # to ease additional file completions. if d.name.endswith(os.sep): if os.name == "nt": path = path + "\\" else: path = path + "/" # Escape to prevent conflicts with the code snippets grammer # See also https://github.com/python-lsp/python-lsp-server/issues/373 if snippet_support: path = path.replace("\\", "\\\\") path = path.replace("/", "\\/") completion["insertText"] = path if include_params and not is_exception_class(d.name): snippet = _snippet(d, resolve_label_or_snippet) completion.update(snippet) return completion def _label(definition, resolve=False): if not resolve: return definition.name sig = LABEL_RESOLVER.get_or_create(definition) if sig: return sig return definition.name def _snippet(definition, resolve=False): if not resolve: return {} snippet = SNIPPET_RESOLVER.get_or_create(definition) return snippet def _detail(definition): try: return definition.parent().full_name or "" except AttributeError: return definition.full_name or "" def _sort_text(definition): """Ensure builtins appear at the bottom. Description is of format : . """ # If its 'hidden', put it next last prefix = "z{}" if definition.name.startswith("_") else "a{}" return prefix.format(definition.name)