1
0
forked from VimPlug/jedi

A first iteration for file path completions

This commit is contained in:
Dave Halter
2019-08-04 13:50:23 +02:00
parent 9dd088f3db
commit fd1e6afd07
8 changed files with 121 additions and 20 deletions

View File

@@ -144,7 +144,6 @@ class Script(object):
'(0-%d) for line %d (%r).' % ( '(0-%d) for line %d (%r).' % (
column, line_len, line, line_string)) column, line_len, line, line_string))
self._pos = line, column self._pos = line, column
self._path = path
cache.clear_time_caches() cache.clear_time_caches()
debug.reset_time() debug.reset_time()

View File

@@ -1,3 +1,5 @@
import re
from parso.python.token import PythonTokenTypes from parso.python.token import PythonTokenTypes
from parso.python import tree from parso.python import tree
from parso.tree import search_ancestor, Leaf from parso.tree import search_ancestor, Leaf
@@ -7,12 +9,13 @@ from jedi import debug
from jedi import settings from jedi import settings
from jedi.api import classes from jedi.api import classes
from jedi.api import helpers from jedi.api import helpers
from jedi.evaluate import imports
from jedi.api import keywords from jedi.api import keywords
from jedi.api.file_name import file_name_completions
from jedi.evaluate import imports
from jedi.evaluate.helpers import evaluate_call_of_leaf, parse_dotted_names from jedi.evaluate.helpers import evaluate_call_of_leaf, parse_dotted_names
from jedi.evaluate.filters import get_global_filters from jedi.evaluate.filters import get_global_filters
from jedi.evaluate.gradual.conversion import convert_contexts from jedi.evaluate.gradual.conversion import convert_contexts
from jedi.parser_utils import get_statement_of_position from jedi.parser_utils import get_statement_of_position, cut_value_at_position
def get_call_signature_param_names(call_signatures): def get_call_signature_param_names(call_signatures):
@@ -121,19 +124,27 @@ class Completion:
""" """
grammar = self._evaluator.grammar grammar = self._evaluator.grammar
self.stack = stack = None
leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True)
string = _extract_string_while_in_string(leaf, self._position)
if string is not None:
completions = list(file_name_completions(self._evaluator, string, self._like_name))
if completions:
return completions
try: try:
self.stack = stack = helpers.get_stack_at_position( self.stack = stack = helpers.get_stack_at_position(
grammar, self._code_lines, self._module_node, self._position grammar, self._code_lines, leaf, self._position
) )
except helpers.OnErrorLeaf as e: except helpers.OnErrorLeaf as e:
self.stack = stack = None value = e.error_leaf.value
if e.error_leaf.value == '.': if value == '.':
# After ErrorLeaf's that are dots, we will not do any # After ErrorLeaf's that are dots, we will not do any
# completions since this probably just confuses the user. # completions since this probably just confuses the user.
return [] return []
# If we don't have a context, just use global completion.
# If we don't have a context, just use global completion.
return self._global_completions() return self._global_completions()
allowed_transitions = \ allowed_transitions = \
@@ -289,3 +300,22 @@ class Completion:
# TODO we should probably check here for properties # TODO we should probably check here for properties
if (name.api_type == 'function') == is_function: if (name.api_type == 'function') == is_function:
yield name yield name
def _extract_string_while_in_string(leaf, position):
if leaf.type == 'string':
match = re.match(r'^\w*(\'{3}|"{3}|\'|")', leaf.value)
quote = match.group(1)
if leaf.line == position[0] and position[1] < leaf.column + match.end():
return None
if leaf.end_pos[0] == position[0] and position[1] > leaf.end_pos[1] - len(quote):
return None
return cut_value_at_position(leaf, position)[match.end():]
leaves = []
while leaf is not None and leaf.line == position[0]:
if leaf.type == 'error_leaf' and ('"' in leaf.value or "'" in leaf.value):
return ''.join(l.get_code() for l in leaves)
leaves.insert(0, leaf)
leaf = leaf.get_previous_leaf()
return None

19
jedi/api/file_name.py Normal file
View File

@@ -0,0 +1,19 @@
import os
from jedi.evaluate.names import AbstractArbitraryName
def file_name_completions(evaluator, string, like_name):
base_path = os.path.join(evaluator.project._path, string)
print(string, base_path)
for name in os.listdir(base_path):
if name.startswith(like_name):
path_for_name = os.path.join(base_path, name)
if os.path.isdir(path_for_name):
name += os.path.sep
yield FileName(evaluator, name)
class FileName(AbstractArbitraryName):
api_type = u'path'
is_context_name = False

View File

@@ -54,8 +54,7 @@ class OnErrorLeaf(Exception):
return self.args[0] return self.args[0]
def _get_code_for_stack(code_lines, module_node, position): def _get_code_for_stack(code_lines, leaf, 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 # It might happen that we're on whitespace or on a comment. This means
# that we would not get the right leaf. # that we would not get the right leaf.
if leaf.start_pos >= position: if leaf.start_pos >= position:
@@ -95,7 +94,7 @@ def _get_code_for_stack(code_lines, module_node, position):
return _get_code(code_lines, user_stmt.get_start_pos_of_prefix(), position) return _get_code(code_lines, user_stmt.get_start_pos_of_prefix(), position)
def get_stack_at_position(grammar, code_lines, module_node, pos): def get_stack_at_position(grammar, code_lines, leaf, pos):
""" """
Returns the possible node names (e.g. import_from, xor_test or yield_stmt). Returns the possible node names (e.g. import_from, xor_test or yield_stmt).
""" """
@@ -119,7 +118,7 @@ def get_stack_at_position(grammar, code_lines, module_node, pos):
yield token yield token
# The code might be indedented, just remove it. # The code might be indedented, just remove it.
code = dedent(_get_code_for_stack(code_lines, module_node, pos)) code = dedent(_get_code_for_stack(code_lines, leaf, pos))
# We use a word to tell Jedi when we have reached the start of the # We use a word to tell Jedi when we have reached the start of the
# completion. # completion.
# Use Z as a prefix because it's not part of a number suffix. # Use Z as a prefix because it's not part of a number suffix.

View File

@@ -21,12 +21,6 @@ def get_operator(evaluator, string, pos):
class KeywordName(AbstractNameDefinition): class KeywordName(AbstractNameDefinition):
api_type = u'keyword' api_type = u'keyword'
is_context_name = False
def __init__(self, evaluator, name):
self.evaluator = evaluator
self.string_name = name
self.parent_context = evaluator.builtins_module
def infer(self): def infer(self):
return [Keyword(self.evaluator, self.string_name, (0, 0))] return [Keyword(self.evaluator, self.string_name, (0, 0))]

View File

@@ -58,6 +58,23 @@ class AbstractNameDefinition(object):
return self.parent_context.api_type return self.parent_context.api_type
class AbstractArbitraryName(AbstractNameDefinition):
"""
When you e.g. want to complete dicts keys, you probably want to complete
string literals, which is not really a name, but for Jedi it works the same
way
"""
is_context_name = False
def __init__(self, evaluator, string):
self.evaluator = evaluator
self.string_name = string
self.parent_context = evaluator.builtins_module
def infer(self):
return NO_CONTEXTS
class AbstractTreeName(AbstractNameDefinition): class AbstractTreeName(AbstractNameDefinition):
def __init__(self, parent_context, tree_name): def __init__(self, parent_context, tree_name):
self.parent_context = parent_context self.parent_context = parent_context

View File

@@ -5,6 +5,7 @@ from weakref import WeakKeyDictionary
from parso.python import tree from parso.python import tree
from parso.cache import parser_cache from parso.cache import parser_cache
from parso import split_lines
from jedi._compatibility import literal_eval, force_unicode from jedi._compatibility import literal_eval, force_unicode
@@ -278,3 +279,15 @@ def get_cached_code_lines(grammar, path):
to do this, but we avoid splitting all the lines again. to do this, but we avoid splitting all the lines again.
""" """
return parser_cache[grammar._hashed][path].lines return parser_cache[grammar._hashed][path].lines
def cut_value_at_position(leaf, position):
"""
Cuts of the value of the leaf at position
"""
lines = split_lines(leaf.value, keepends=True)[:position[0] - leaf.line + 1]
column = position[1]
if leaf.line == position[0]:
column -= leaf.column
lines[-1] = lines[-1][:column]
return ''.join(lines)

View File

@@ -1,8 +1,9 @@
import os from os.path import join, sep as s
import sys import sys
from textwrap import dedent from textwrap import dedent
import pytest import pytest
from ..helpers import root_dir
def test_in_whitespace(Script): def test_in_whitespace(Script):
@@ -69,8 +70,8 @@ def test_points_in_completion(Script):
def test_loading_unicode_files_with_bad_global_charset(Script, monkeypatch, tmpdir): def test_loading_unicode_files_with_bad_global_charset(Script, monkeypatch, tmpdir):
dirname = str(tmpdir.mkdir('jedi-test')) dirname = str(tmpdir.mkdir('jedi-test'))
filename1 = os.path.join(dirname, 'test1.py') filename1 = join(dirname, 'test1.py')
filename2 = os.path.join(dirname, 'test2.py') filename2 = join(dirname, 'test2.py')
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
data = "# coding: latin-1\nfoo = 'm\xf6p'\n" data = "# coding: latin-1\nfoo = 'm\xf6p'\n"
else: else:
@@ -156,3 +157,32 @@ def test_with_stmt_error_recovery(Script):
) )
def test_keyword_completion(Script, code, has_keywords): def test_keyword_completion(Script, code, has_keywords):
assert has_keywords == any(x.is_keyword for x in Script(code).completions()) assert has_keywords == any(x.is_keyword for x in Script(code).completions())
@pytest.mark.parametrize(
'file, code, column, expected', [
# General tests / relative paths
(None, '"comp', None, ['ile', 'lex']), # No files like comp
(None, '"test', None, [s]),
(None, '"test', 4, ['t' + s]),
('example.py', '"test%scomp' % s, None, ['letion' + s]),
('example.py', 'r"comp"', None, ...),
('example.py', 'r"tes"', None, ...),
('example.py', 'r"tes"', 5, ['t' + s]),
('test%sexample.py' % s, 'r"tes"', 5, ['t' + s]),
('test%sexample.py' % s, 'r"test%scomp"' % s, 5, ['t' + s]),
('test%sexample.py' % s, 'r"test%scomp"' % s, 11, ['letion' + s]),
('test%sexample.py' % s, 'r"%s"' % join('test', 'completion', 'basi'), 22, ['c.py']),
('example.py', 'rb"' + join('..', 'jedi', 'tes'), None, ['t' + s]),
# Absolute paths
(None, '"' + join(root_dir, 'test', 'test_ca'), None, ['che.py']),
(None, '"%s"' % join(root_dir, 'test', 'test_ca'), len(root_dir) + 14, ['che.py']),
]
)
def test_file_path_completions(Script, file, code, column, expected):
comps = Script(code, path=file, column=column).completions()
if expected == ...:
assert len(comps) > 100 # This is basically global completions.
else:
assert [c.complete for c in comps] == expected