316 lines
11 KiB
Python
316 lines
11 KiB
Python
# 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 <type>: <module>.<item>
|
|
"""
|
|
|
|
# If its 'hidden', put it next last
|
|
prefix = "z{}" if definition.name.startswith("_") else "a{}"
|
|
return prefix.format(definition.name)
|