client-py/venv/lib/python3.12/site-packages/pylsp/plugins/jedi_completion.py
2026-05-02 13:34:53 +05:00

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)