Skip to content

Commit

Permalink
Only add snippet completions when positional args are available (#734)
Browse files Browse the repository at this point in the history
  • Loading branch information
dalthviz authored Feb 5, 2020
1 parent 3ce17bd commit f6df42c
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 18 deletions.
68 changes: 52 additions & 16 deletions pyls/plugins/jedi_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
# Types of parso nodes for which snippet is not included in the completion
_IMPORTS = ('import_name', 'import_from')

# Types of parso node for errors
_ERRORS = ('error_node', )


@hookimpl
def pyls_completions(config, document, position):
Expand All @@ -63,11 +66,25 @@ def pyls_completions(config, document, position):

settings = config.plugin_settings('jedi_completion', document_path=document.path)
should_include_params = settings.get('include_params')
include_params = (snippet_support and should_include_params and
use_snippets(document, position))
include_params = snippet_support and should_include_params and use_snippets(document, position)
return [_format_completion(d, include_params) for d in definitions] or None


def is_exception_class(name):
"""
Determine if a class name is an instance of an Exception.
This returns `False` if the name given corresponds with a instance of
the 'Exception' class, `True` otherwise
"""
try:
return name in [cls.__name__ for cls in Exception.__subclasses__()]
except AttributeError:
# Needed in case a class don't uses new-style
# class definition in Python 2
return False


def use_snippets(document, position):
"""
Determine if it's necessary to return snippets in code completions.
Expand All @@ -79,15 +96,28 @@ def use_snippets(document, position):
lines = document.source.split('\n', line)
act_lines = [lines[line][:position['character']]]
line -= 1
last_character = ''
while line > -1:
act_line = lines[line]
if act_line.rstrip().endswith('\\'):
if (act_line.rstrip().endswith('\\') or
act_line.rstrip().endswith('(') or
act_line.rstrip().endswith(',')):
act_lines.insert(0, act_line)
line -= 1
if act_line.rstrip().endswith('('):
# Needs to be added to the end of the code before parsing
# to make it valid, otherwise the node type could end
# being an 'error_node' for multi-line imports that use '('
last_character = ')'
else:
break
tokens = parso.parse('\n'.join(act_lines).split(';')[-1].strip())
return tokens.children[0].type not in _IMPORTS
if '(' in act_lines[-1].strip():
last_character = ')'
code = '\n'.join(act_lines).split(';')[-1].strip() + last_character
tokens = parso.parse(code)
expr_type = tokens.children[0].type
return (expr_type not in _IMPORTS and
not (expr_type in _ERRORS and 'import' in code))


def _format_completion(d, include_params=True):
Expand All @@ -100,19 +130,25 @@ def _format_completion(d, include_params=True):
'insertText': d.name
}

if include_params and hasattr(d, 'params') and d.params:
if (include_params and hasattr(d, 'params') and d.params and
not is_exception_class(d.name)):
positional_args = [param for param in d.params if '=' not in param.description]

# For completions with params, we can generate a snippet instead
completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
snippet = d.name + '('
for i, param in enumerate(positional_args):
name = param.name if param.name != '/' else '\\/'
snippet += '${%s:%s}' % (i + 1, name)
if i < len(positional_args) - 1:
snippet += ', '
snippet += ')$0'
completion['insertText'] = snippet
if len(positional_args) > 1:
# For completions with params, we can generate a snippet instead
completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
snippet = d.name + '('
for i, param in enumerate(positional_args):
name = param.name if param.name != '/' else '\\/'
snippet += '${%s:%s}' % (i + 1, name)
if i < len(positional_args) - 1:
snippet += ', '
snippet += ')$0'
completion['insertText'] = snippet
elif len(positional_args) == 1:
completion['insertText'] = d.name + '($0)'
else:
completion['insertText'] = d.name + '()'

return completion

Expand Down
19 changes: 17 additions & 2 deletions test/plugins/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,7 @@ def test_snippets_completion(config):

com_position = {'line': 1, 'character': len(doc_snippets)}
completions = pyls_jedi_completions(config, doc, com_position)
out = 'defaultdict(${1:kwargs})$0'
assert completions[0]['insertText'] == out
assert completions[0]['insertText'] == 'defaultdict($0)'


def test_snippet_parsing(config):
Expand All @@ -205,6 +204,22 @@ def test_snippet_parsing(config):
assert completions[0]['insertText'] == out


def test_multiline_import_snippets(config):
document = 'from datetime import(\n date,\n datetime)\na=date'
doc = Document(DOC_URI, document)
config.capabilities['textDocument'] = {
'completion': {'completionItem': {'snippetSupport': True}}}
config.update({'plugins': {'jedi_completion': {'include_params': True}}})

position = {'line': 1, 'character': 5}
completions = pyls_jedi_completions(config, doc, position)
assert completions[0]['insertText'] == 'date'

position = {'line': 2, 'character': 9}
completions = pyls_jedi_completions(config, doc, position)
assert completions[0]['insertText'] == 'datetime'


def test_multiline_snippets(config):
document = 'from datetime import\\\n date,\\\n datetime \na=date'
doc = Document(DOC_URI, document)
Expand Down

0 comments on commit f6df42c

Please sign in to comment.