You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
9.1 KiB
261 lines
9.1 KiB
"""
|
|
Helpers for the API
|
|
"""
|
|
import re
|
|
from collections import namedtuple
|
|
from textwrap import dedent
|
|
|
|
from parso.python.parser import Parser
|
|
from parso.python import tree
|
|
|
|
from jedi._compatibility import u
|
|
from jedi.evaluate.syntax_tree import eval_atom
|
|
from jedi.evaluate.helpers import evaluate_call_of_leaf
|
|
from jedi.evaluate.compiled import get_string_context_set
|
|
from jedi.cache import call_signature_time_cache
|
|
|
|
|
|
CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name'])
|
|
|
|
|
|
def sorted_definitions(defs):
|
|
# Note: `or ''` below is required because `module_path` could be
|
|
return sorted(defs, key=lambda x: (x.module_path or '', x.line or 0, x.column or 0))
|
|
|
|
|
|
def get_on_completion_name(module_node, lines, position):
|
|
leaf = module_node.get_leaf_for_position(position)
|
|
if leaf is None or leaf.type in ('string', 'error_leaf'):
|
|
# Completions inside strings are a bit special, we need to parse the
|
|
# string. The same is true for comments and error_leafs.
|
|
line = lines[position[0] - 1]
|
|
# The first step of completions is to get the name
|
|
return re.search(r'(?!\d)\w+$|$', line[:position[1]]).group(0)
|
|
elif leaf.type not in ('name', 'keyword'):
|
|
return ''
|
|
|
|
return leaf.value[:position[1] - leaf.start_pos[1]]
|
|
|
|
|
|
def _get_code(code_lines, start_pos, end_pos):
|
|
# Get relevant lines.
|
|
lines = code_lines[start_pos[0] - 1:end_pos[0]]
|
|
# Remove the parts at the end of the line.
|
|
lines[-1] = lines[-1][:end_pos[1]]
|
|
# Remove first line indentation.
|
|
lines[0] = lines[0][start_pos[1]:]
|
|
return ''.join(lines)
|
|
|
|
|
|
class OnErrorLeaf(Exception):
|
|
@property
|
|
def error_leaf(self):
|
|
return self.args[0]
|
|
|
|
|
|
def _get_code_for_stack(code_lines, module_node, position):
|
|
leaf = module_node.get_leaf_for_position(position, include_prefixes=True)
|
|
# It might happen that we're on whitespace or on a comment. This means
|
|
# that we would not get the right leaf.
|
|
if leaf.start_pos >= position:
|
|
# If we're not on a comment simply get the previous leaf and proceed.
|
|
leaf = leaf.get_previous_leaf()
|
|
if leaf is None:
|
|
return u('') # At the beginning of the file.
|
|
|
|
is_after_newline = leaf.type == 'newline'
|
|
while leaf.type == 'newline':
|
|
leaf = leaf.get_previous_leaf()
|
|
if leaf is None:
|
|
return u('')
|
|
|
|
if leaf.type == 'error_leaf' or leaf.type == 'string':
|
|
if leaf.start_pos[0] < position[0]:
|
|
# On a different line, we just begin anew.
|
|
return u('')
|
|
|
|
# Error leafs cannot be parsed, completion in strings is also
|
|
# impossible.
|
|
raise OnErrorLeaf(leaf)
|
|
else:
|
|
user_stmt = leaf
|
|
while True:
|
|
if user_stmt.parent.type in ('file_input', 'suite', 'simple_stmt'):
|
|
break
|
|
user_stmt = user_stmt.parent
|
|
|
|
if is_after_newline:
|
|
if user_stmt.start_pos[1] > position[1]:
|
|
# This means that it's actually a dedent and that means that we
|
|
# start without context (part of a suite).
|
|
return u('')
|
|
|
|
# This is basically getting the relevant lines.
|
|
return _get_code(code_lines, user_stmt.get_start_pos_of_prefix(), position)
|
|
|
|
|
|
def get_stack_at_position(grammar, code_lines, module_node, pos):
|
|
"""
|
|
Returns the possible node names (e.g. import_from, xor_test or yield_stmt).
|
|
"""
|
|
class EndMarkerReached(Exception):
|
|
pass
|
|
|
|
def tokenize_without_endmarker(code):
|
|
# TODO This is for now not an official parso API that exists purely
|
|
# for Jedi.
|
|
tokens = grammar._tokenize(code)
|
|
for token in tokens:
|
|
if token.string == safeword:
|
|
raise EndMarkerReached()
|
|
elif token.prefix.endswith(safeword):
|
|
# This happens with comments.
|
|
raise EndMarkerReached()
|
|
elif token.string.endswith(safeword):
|
|
yield token # Probably an f-string literal that was not finished.
|
|
raise EndMarkerReached()
|
|
else:
|
|
yield token
|
|
|
|
# The code might be indedented, just remove it.
|
|
code = dedent(_get_code_for_stack(code_lines, module_node, pos))
|
|
# We use a word to tell Jedi when we have reached the start of the
|
|
# completion.
|
|
# Use Z as a prefix because it's not part of a number suffix.
|
|
safeword = 'ZZZ_USER_WANTS_TO_COMPLETE_HERE_WITH_JEDI'
|
|
code = code + ' ' + safeword
|
|
|
|
p = Parser(grammar._pgen_grammar, error_recovery=True)
|
|
try:
|
|
p.parse(tokens=tokenize_without_endmarker(code))
|
|
except EndMarkerReached:
|
|
return p.stack
|
|
raise SystemError(
|
|
"This really shouldn't happen. There's a bug in Jedi:\n%s"
|
|
% list(tokenize_without_endmarker(code))
|
|
)
|
|
|
|
|
|
def evaluate_goto_definition(evaluator, context, leaf):
|
|
if leaf.type == 'name':
|
|
# In case of a name we can just use goto_definition which does all the
|
|
# magic itself.
|
|
return evaluator.goto_definitions(context, leaf)
|
|
|
|
parent = leaf.parent
|
|
if parent.type == 'atom':
|
|
return context.eval_node(leaf.parent)
|
|
elif parent.type == 'trailer':
|
|
return evaluate_call_of_leaf(context, leaf)
|
|
elif isinstance(leaf, tree.Literal):
|
|
return eval_atom(context, leaf)
|
|
elif leaf.type in ('fstring_string', 'fstring_start', 'fstring_end'):
|
|
return get_string_context_set(evaluator)
|
|
return []
|
|
|
|
|
|
CallSignatureDetails = namedtuple(
|
|
'CallSignatureDetails',
|
|
['bracket_leaf', 'call_index', 'keyword_name_str']
|
|
)
|
|
|
|
|
|
def _get_index_and_key(nodes, position):
|
|
"""
|
|
Returns the amount of commas and the keyword argument string.
|
|
"""
|
|
nodes_before = [c for c in nodes if c.start_pos < position]
|
|
if nodes_before[-1].type == 'arglist':
|
|
nodes_before = [c for c in nodes_before[-1].children if c.start_pos < position]
|
|
|
|
key_str = None
|
|
|
|
if nodes_before:
|
|
last = nodes_before[-1]
|
|
if last.type == 'argument' and last.children[1] == '=' \
|
|
and last.children[1].end_pos <= position:
|
|
# Checked if the argument
|
|
key_str = last.children[0].value
|
|
elif last == '=':
|
|
key_str = nodes_before[-2].value
|
|
|
|
return nodes_before.count(','), key_str
|
|
|
|
|
|
def _get_call_signature_details_from_error_node(node, position):
|
|
for index, element in reversed(list(enumerate(node.children))):
|
|
# `index > 0` means that it's a trailer and not an atom.
|
|
if element == '(' and element.end_pos <= position and index > 0:
|
|
# It's an error node, we don't want to match too much, just
|
|
# until the parentheses is enough.
|
|
children = node.children[index:]
|
|
name = element.get_previous_leaf()
|
|
if name is None:
|
|
continue
|
|
if name.type == 'name' or name.parent.type in ('trailer', 'atom'):
|
|
return CallSignatureDetails(
|
|
element,
|
|
*_get_index_and_key(children, position)
|
|
)
|
|
|
|
|
|
def get_call_signature_details(module, position):
|
|
leaf = module.get_leaf_for_position(position, include_prefixes=True)
|
|
if leaf.start_pos >= position:
|
|
# Whitespace / comments after the leaf count towards the previous leaf.
|
|
leaf = leaf.get_previous_leaf()
|
|
if leaf is None:
|
|
return None
|
|
|
|
if leaf == ')':
|
|
if leaf.end_pos == position:
|
|
leaf = leaf.get_next_leaf()
|
|
|
|
# Now that we know where we are in the syntax tree, we start to look at
|
|
# parents for possible function definitions.
|
|
node = leaf.parent
|
|
while node is not None:
|
|
if node.type in ('funcdef', 'classdef'):
|
|
# Don't show call signatures if there's stuff before it that just
|
|
# makes it feel strange to have a call signature.
|
|
return None
|
|
|
|
for n in node.children[::-1]:
|
|
if n.start_pos < position and n.type == 'error_node':
|
|
result = _get_call_signature_details_from_error_node(n, position)
|
|
if result is not None:
|
|
return result
|
|
|
|
if node.type == 'trailer' and node.children[0] == '(':
|
|
leaf = node.get_previous_leaf()
|
|
if leaf is None:
|
|
return None
|
|
return CallSignatureDetails(
|
|
node.children[0], *_get_index_and_key(node.children, position))
|
|
|
|
node = node.parent
|
|
|
|
return None
|
|
|
|
|
|
@call_signature_time_cache("call_signatures_validity")
|
|
def cache_call_signatures(evaluator, context, bracket_leaf, code_lines, user_pos):
|
|
"""This function calculates the cache key."""
|
|
line_index = user_pos[0] - 1
|
|
|
|
before_cursor = code_lines[line_index][:user_pos[1]]
|
|
other_lines = code_lines[bracket_leaf.start_pos[0]:line_index]
|
|
whole = ''.join(other_lines + [before_cursor])
|
|
before_bracket = re.match(r'.*\(', whole, re.DOTALL)
|
|
|
|
module_path = context.get_root_context().py__file__()
|
|
if module_path is None:
|
|
yield None # Don't cache!
|
|
else:
|
|
yield (module_path, before_bracket, bracket_leaf.start_pos)
|
|
yield evaluate_goto_definition(
|
|
evaluator,
|
|
context,
|
|
bracket_leaf.get_previous_leaf()
|
|
)
|