forked from VimPlug/jedi
A first iteration for file path completions
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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
19
jedi/api/file_name.py
Normal 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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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))]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user