diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index c93730a3..ac0f4c76 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -144,7 +144,6 @@ class Script(object): '(0-%d) for line %d (%r).' % ( column, line_len, line, line_string)) self._pos = line, column - self._path = path cache.clear_time_caches() debug.reset_time() diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 48f002ba..ee91025c 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -1,3 +1,5 @@ +import re + from parso.python.token import PythonTokenTypes from parso.python import tree from parso.tree import search_ancestor, Leaf @@ -7,12 +9,13 @@ from jedi import debug from jedi import settings from jedi.api import classes from jedi.api import helpers -from jedi.evaluate import imports 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.filters import get_global_filters 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): @@ -121,19 +124,27 @@ class Completion: """ 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: 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: - self.stack = stack = None - if e.error_leaf.value == '.': + value = e.error_leaf.value + if value == '.': # After ErrorLeaf's that are dots, we will not do any # completions since this probably just confuses the user. 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() allowed_transitions = \ @@ -289,3 +300,22 @@ class Completion: # TODO we should probably check here for properties if (name.api_type == 'function') == is_function: 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 diff --git a/jedi/api/file_name.py b/jedi/api/file_name.py new file mode 100644 index 00000000..c740baa3 --- /dev/null +++ b/jedi/api/file_name.py @@ -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 diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index 8eedff97..6fafb116 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -54,8 +54,7 @@ class OnErrorLeaf(Exception): 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) +def _get_code_for_stack(code_lines, leaf, position): # 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: @@ -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) -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). """ @@ -119,7 +118,7 @@ def get_stack_at_position(grammar, code_lines, module_node, pos): yield token # 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 # completion. # Use Z as a prefix because it's not part of a number suffix. diff --git a/jedi/api/keywords.py b/jedi/api/keywords.py index 3e76ad1e..10cf28bf 100644 --- a/jedi/api/keywords.py +++ b/jedi/api/keywords.py @@ -21,12 +21,6 @@ def get_operator(evaluator, string, pos): class KeywordName(AbstractNameDefinition): 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): return [Keyword(self.evaluator, self.string_name, (0, 0))] diff --git a/jedi/evaluate/names.py b/jedi/evaluate/names.py index bf891e82..81be899a 100644 --- a/jedi/evaluate/names.py +++ b/jedi/evaluate/names.py @@ -58,6 +58,23 @@ class AbstractNameDefinition(object): 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): def __init__(self, parent_context, tree_name): self.parent_context = parent_context diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index dcbd802c..6b590112 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -5,6 +5,7 @@ from weakref import WeakKeyDictionary from parso.python import tree from parso.cache import parser_cache +from parso import split_lines 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. """ 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) diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index ccf77809..293c8577 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -1,8 +1,9 @@ -import os +from os.path import join, sep as s import sys from textwrap import dedent import pytest +from ..helpers import root_dir 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): dirname = str(tmpdir.mkdir('jedi-test')) - filename1 = os.path.join(dirname, 'test1.py') - filename2 = os.path.join(dirname, 'test2.py') + filename1 = join(dirname, 'test1.py') + filename2 = join(dirname, 'test2.py') if sys.version_info < (3, 0): data = "# coding: latin-1\nfoo = 'm\xf6p'\n" else: @@ -156,3 +157,32 @@ def test_with_stmt_error_recovery(Script): ) def test_keyword_completion(Script, code, has_keywords): 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