diff --git a/jedi/api/completion.py b/jedi/api/completion.py index b25b6b8d..deef40b6 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -10,7 +10,7 @@ from jedi import settings from jedi.api import classes from jedi.api import helpers from jedi.api import keywords -from jedi.api.strings import completions_for_dicts +from jedi.api.strings import complete_dict from jedi.api.file_name import complete_file_name from jedi.inference import imports from jedi.inference.base_value import ValueSet @@ -105,21 +105,13 @@ class Completion: leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True) string, start_leaf = _extract_string_while_in_string(leaf, self._position) - prefixed_completions = [] - #if string is None: - #string = '' - bracket_leaf = leaf - #if bracket_leaf.type in ('number', 'error_leaf'): - #string = bracket_leaf.value - #bracket_leaf = bracket_leaf.get_previous_leaf() - - if bracket_leaf == '[': - context = self._module_context.create_context(bracket_leaf) - before_bracket_leaf = bracket_leaf.get_previous_leaf() - if before_bracket_leaf.type in ('atom', 'trailer', 'name'): - values = infer_call_of_leaf(context, before_bracket_leaf) - prefixed_completions += completions_for_dicts( - self._inference_state, values, string, fuzzy=fuzzy) + prefixed_completions = complete_dict( + self._module_context, + leaf, + self._original_position, + string, + fuzzy=fuzzy, + ) if string is not None and not prefixed_completions: prefixed_completions = list(complete_file_name( @@ -128,8 +120,6 @@ class Completion: self._code_lines, self._original_position, fuzzy )) - if prefixed_completions: - return prefixed_completions if string is not None: return prefixed_completions diff --git a/jedi/api/strings.py b/jedi/api/strings.py index 4b89f821..3c9d4929 100644 --- a/jedi/api/strings.py +++ b/jedi/api/strings.py @@ -7,9 +7,13 @@ It however does the same for numbers. The difference between string completions and other completions is mostly that this module doesn't return defined names in a module, but pretty much an arbitrary string. """ +import re + +from jedi._compatibility import unicode from jedi.inference.names import AbstractArbitraryName +from jedi.inference.helpers import infer_call_of_leaf from jedi.api.classes import Completion -from jedi.parser_utils import get_string_quote +from jedi.parser_utils import cut_value_at_position _sentinel = object() @@ -19,11 +23,46 @@ class StringName(AbstractArbitraryName): is_value_name = False -def completions_for_dicts(inference_state, dicts, literal_string, fuzzy): - for dict_key in sorted(_get_python_keys(dicts)): - dict_key_str = repr(dict_key) +def complete_dict(module_context, leaf, position, string, fuzzy): + if string is None: + string = '' + bracket_leaf = leaf + end_quote = '' + if bracket_leaf.type in ('number', 'error_leaf'): + string = cut_value_at_position(bracket_leaf, position) + if bracket_leaf.end_pos > position: + end_quote = _get_string_quote(string) or '' + if end_quote: + ending = cut_value_at_position( + bracket_leaf, + (position[0], position[1] + len(end_quote)) + ) + if not ending.endswith(end_quote): + end_quote = '' + + bracket_leaf = bracket_leaf.get_previous_leaf() + + if bracket_leaf == '[': + context = module_context.create_context(bracket_leaf) + before_bracket_leaf = bracket_leaf.get_previous_leaf() + if before_bracket_leaf.type in ('atom', 'trailer', 'name'): + values = infer_call_of_leaf(context, before_bracket_leaf) + return list(_completions_for_dicts( + module_context.inference_state, + values, + '' if string is None else string, + end_quote, + fuzzy=fuzzy, + )) + return [] + + +def _completions_for_dicts(inference_state, dicts, literal_string, end_quote, fuzzy): + for dict_key in sorted(_get_python_keys(dicts), key=lambda x: repr(x)): + dict_key_str = _create_repr_string(literal_string, dict_key) if dict_key_str.startswith(literal_string): - name = StringName(inference_state, dict_key_str[len(literal_string):]) + n = dict_key_str[len(literal_string):-len(end_quote) or None] + name = StringName(inference_state, n) yield Completion( inference_state, name, @@ -33,6 +72,17 @@ def completions_for_dicts(inference_state, dicts, literal_string, fuzzy): ) +def _create_repr_string(literal_string, dict_key): + if not isinstance(dict_key, (unicode, bytes)) or not literal_string: + return repr(dict_key) + + r = repr(dict_key) + prefix, quote = _get_string_prefix_and_quote(literal_string) + if quote == r[0]: + return prefix + r + return prefix + quote + r[1:-1] + quote + + def _get_python_keys(dicts): for dct in dicts: if dct.array_type == 'dict': @@ -42,9 +92,20 @@ def _get_python_keys(dicts): yield dict_key +def _get_string_prefix_and_quote(string): + match = re.match(r'(\w*)("""|\'{3}|"|\')', string) + if match is None: + return None, None + return match.group(1), match.group(2) + + +def _get_string_quote(string): + return _get_string_prefix_and_quote(string)[1] + + def get_quote_ending(start_leaf, code_lines, position): if start_leaf.type == 'string': - quote = get_string_quote(start_leaf) + quote = _get_string_quote(start_leaf) else: assert start_leaf.type == 'error_leaf' quote = start_leaf.value diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index aa6b7290..6fe5cd36 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -354,6 +354,9 @@ class TreeInstance(_BaseTreeInstance): def get_annotated_class_object(self): return self._get_annotated_class_object() or self.class_value + def get_key_values(self): + return NO_VALUES + def py__simple_getitem__(self, index): if self.array_type == 'dict': # Logic for dict({'foo': bar}) and dict(foo=bar) diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index f8537858..0f7ba429 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -293,10 +293,6 @@ def cut_value_at_position(leaf, position): return ''.join(lines) -def get_string_quote(leaf): - return re.match(r'\w*("""|\'{3}|"|\')', leaf.value).group(1) - - def _function_is_x_method(method_name): def wrapper(function_node): """ diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index cf65fb22..41da9c7d 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -175,7 +175,7 @@ current_dirname = os.path.basename(dirname(dirname(dirname(__file__)))) @pytest.mark.parametrize( 'file, code, column, expected', [ # General tests / relative paths - (None, '"comp', None, ['ile', 'lex']), # No files like comp + (None, '"comp', None, []), # No files like comp (None, '"test', None, [s]), (None, '"test', 4, ['t' + s]), ('example.py', '"test%scomp' % s, None, ['letion' + s]), @@ -273,8 +273,7 @@ def test_file_path_completions(Script, file, code, column, expected): assert [c.complete for c in comps] == expected -@pytest.mark.parametrize( - 'added_code, column, expected', [ +_dict_keys_completion_tests = [ ('ints[', 5, ['1', '50', Ellipsis]), ('ints[]', 5, ['1', '50', Ellipsis]), ('ints[1]', 5, ['1', '50', Ellipsis]), @@ -286,19 +285,20 @@ def test_file_path_completions(Script, file, code, column, expected): ('ints[5]', 6, ['0']), ('ints[50', 5, ['1', '50', Ellipsis]), ('ints[5', 6, ['0']), - ('ints[50', 6, ['']),#TODO ['0']), + ('ints[50', 6, ['0']), ('ints[50', 7, ['']), ('strs[', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), ('strs[]', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), + ("strs['", 6, ["asdf'", "fbar'", "foo'"]), ("strs[']", 6, ["asdf'", "fbar'", "foo'"]), ('strs["]', 6, ['asdf"', 'fbar"', 'foo"']), - ('strs["""]', 6, ['asdf', 'bar', 'foo']), + ('strs["""]', 6, ['asdf', 'fbar', 'foo']), ('strs["""]', 8, ['asdf"""', 'fbar"""', 'foo"""']), ('strs[b"]', 8, []), - ('strs[r"asd', 11, ['f"']), - ('strs[R"asd', 11, ['f"']), - ('strs[f"asd', 11, ['f"']), + ('strs[r"asd', 10, ['f"']), + ('strs[R"asd', 10, ['f"']), + ('strs[f"asd', 10, ['f"']), ('strs["f', 7, ['oo"]']), ('strs["f"', 7, ['oo']), @@ -310,7 +310,11 @@ def test_file_path_completions(Script, file, code, column, expected): ('casted["f', 9, ['3"', 'bar"', 'oo"']), ('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'uuu"', 'ull"']), - ] +] + + +@pytest.mark.parametrize( + 'added_code, column, expected', _dict_keys_completion_tests ) def test_dict_keys_completions(Script, added_code, column, expected, skip_pre_python35): code = dedent(r'''