diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index a052b872..2073aab0 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -204,9 +204,9 @@ class Script(object): with debug.increase_indent_cm('complete'): completion = Completion( self._inference_state, self._get_module_context(), self._code_lines, - (line, column), self.find_signatures + (line, column), self.find_signatures, fuzzy=fuzzy, ) - return completion.complete(fuzzy) + return completion.complete() def completions(self, fuzzy=False): # Deprecated, will be removed. diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 18cd8082..9ec77978 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -1,8 +1,10 @@ import re +from textwrap import dedent from parso.python.token import PythonTokenTypes from parso.python import tree from parso.tree import search_ancestor, Leaf +from parso import split_lines from jedi._compatibility import Parameter from jedi import debug @@ -16,7 +18,7 @@ from jedi.inference import imports from jedi.inference.base_value import ValueSet from jedi.inference.helpers import infer_call_of_leaf, parse_dotted_names from jedi.inference.context import get_global_filters -from jedi.inference.value import TreeInstance +from jedi.inference.value import TreeInstance, ModuleValue from jedi.inference.gradual.conversion import convert_values from jedi.parser_utils import cut_value_at_position from jedi.plugins import plugin_manager @@ -105,7 +107,7 @@ class Completion: self._fuzzy = fuzzy - def complete(self, fuzzy): + def complete(self): leaf = self._module_node.get_leaf_for_position( self._original_position, include_prefixes=True @@ -118,7 +120,7 @@ class Completion: start_leaf or leaf, self._original_position, None if string is None else quote + string, - fuzzy=fuzzy, + fuzzy=self._fuzzy, ) if string is not None and not prefixed_completions: @@ -126,15 +128,18 @@ class Completion: self._inference_state, self._module_context, start_leaf, string, self._like_name, self._signatures_callback, self._code_lines, self._original_position, - fuzzy + self._fuzzy )) if string is not None: + if not prefixed_completions and '\n' in string: + # Complete only multi line strings + prefixed_completions = self._complete_in_string(start_leaf, string) return prefixed_completions completion_names = self._complete_python(leaf) completions = list(filter_names(self._inference_state, completion_names, - self.stack, self._like_name, fuzzy)) + self.stack, self._like_name, self._fuzzy)) return ( # Removing duplicates mostly to remove False/True/None duplicates. @@ -434,6 +439,41 @@ class Completion: if (name.api_type == 'function') == is_function: yield name + def _complete_in_string(self, start_leaf, string): + """ + To make it possible for people to have completions in doctests or + generally in "Python" code in docstrings, we use the following + heuristic: + + - Either + """ + string = dedent(string) + code_lines = split_lines(string, keepends=True) + if code_lines[-1].startswith('>>>') or code_lines[-1].startswith(' '): + code_lines = [ + re.sub(r'^(>>> ?| +)', '', l) + for l in code_lines + if l.startswith('>>>') or l.startswith(' ') + ] + module_node = self._inference_state.grammar.parse(''.join(code_lines)) + module_value = ModuleValue( + self._inference_state, + module_node, + file_io=None, + string_names=None, + code_lines=code_lines, + ) + module_value.parent_context = self._module_context + return Completion( + self._inference_state, + module_value.as_context(), + code_lines=code_lines, + position=module_node.end_pos, + signatures_callback=lambda *args, **kwargs: [], + fuzzy=self._fuzzy + ).complete() + return [] + def _gather_nodes(stack): nodes = [] @@ -466,7 +506,7 @@ def _extract_string_while_in_string(leaf, position): return return_part_of_leaf(leaf) leaves = [] - while leaf is not None and leaf.line == position[0]: + while leaf is not None: if leaf.type == 'error_leaf' and ('"' in leaf.value or "'" in leaf.value): if len(leaf.value) > 1: return return_part_of_leaf(leaf) @@ -483,6 +523,12 @@ def _extract_string_while_in_string(leaf, position): ('' if prefix_leaf is None else prefix_leaf.value) + cut_value_at_position(leaf, position), ) + if leaf.line != position[0]: + # Multi line strings are always simple error leaves and contain the + # whole string, single line error leaves are atherefore important + # now and since the line is different, it's not really a single + # line string anymore. + break leaves.insert(0, leaf) leaf = leaf.get_previous_leaf() return None, None, None diff --git a/jedi/inference/context.py b/jedi/inference/context.py index e500707d..bf43bd4b 100644 --- a/jedi/inference/context.py +++ b/jedi/inference/context.py @@ -468,7 +468,7 @@ def get_global_filters(context, until_position, origin_scope): until_position=until_position, origin_scope=origin_scope): yield filter - if isinstance(context, BaseFunctionExecutionContext): + if isinstance(context, (BaseFunctionExecutionContext, ModuleContext)): # The position should be reset if the current scope is a function. until_position = None diff --git a/test/completion/docstring.py b/test/completion/docstring.py index 2b9f3481..b0c61487 100644 --- a/test/completion/docstring.py +++ b/test/completion/docstring.py @@ -252,3 +252,46 @@ def import_issues(foo): """ #? datetime.datetime() foo + + +# ----------------- +# Doctest completions +# ----------------- + +def doctest_with_gt(): + """ + x + + >>> somewhere_in_docstring = 3 + #? ['import_issues'] + >>> import_issu + #? ['somewhere_in_docstring'] + >>> somewhere_ + + blabla + + >>> haha = 3 + #? ['haha'] + >>> hah + #? ['doctest_with_space'] + >>> doctest_with_sp + """ + +def doctest_with_space(): + """ + x + #? ['import_issues'] + import_issu + """ + +def doctest_without_ending(): + """ + #? [] + import_issu + ha + + no_ending = False + #? ['import_issues'] + import_issu + #? ['no_ending'] + no_endin diff --git a/test/test_parso_integration/test_error_correction.py b/test/test_parso_integration/test_error_correction.py index 3ecb32e1..b7796817 100644 --- a/test/test_parso_integration/test_error_correction.py +++ b/test/test_parso_integration/test_error_correction.py @@ -23,7 +23,7 @@ def test_string_literals(Script): script = Script(dedent(source)) assert script._get_module_context().tree_node.end_pos == (6, 0) - assert script.complete() + assert not script.complete() def test_incomplete_function(Script):