Skip to content

Commit f6df42c

Browse files
authored
Only add snippet completions when positional args are available (#734)
1 parent 3ce17bd commit f6df42c

File tree

2 files changed

+69
-18
lines changed

2 files changed

+69
-18
lines changed

pyls/plugins/jedi_completion.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
# Types of parso nodes for which snippet is not included in the completion
4343
_IMPORTS = ('import_name', 'import_from')
4444

45+
# Types of parso node for errors
46+
_ERRORS = ('error_node', )
47+
4548

4649
@hookimpl
4750
def pyls_completions(config, document, position):
@@ -63,11 +66,25 @@ def pyls_completions(config, document, position):
6366

6467
settings = config.plugin_settings('jedi_completion', document_path=document.path)
6568
should_include_params = settings.get('include_params')
66-
include_params = (snippet_support and should_include_params and
67-
use_snippets(document, position))
69+
include_params = snippet_support and should_include_params and use_snippets(document, position)
6870
return [_format_completion(d, include_params) for d in definitions] or None
6971

7072

73+
def is_exception_class(name):
74+
"""
75+
Determine if a class name is an instance of an Exception.
76+
77+
This returns `False` if the name given corresponds with a instance of
78+
the 'Exception' class, `True` otherwise
79+
"""
80+
try:
81+
return name in [cls.__name__ for cls in Exception.__subclasses__()]
82+
except AttributeError:
83+
# Needed in case a class don't uses new-style
84+
# class definition in Python 2
85+
return False
86+
87+
7188
def use_snippets(document, position):
7289
"""
7390
Determine if it's necessary to return snippets in code completions.
@@ -79,15 +96,28 @@ def use_snippets(document, position):
7996
lines = document.source.split('\n', line)
8097
act_lines = [lines[line][:position['character']]]
8198
line -= 1
99+
last_character = ''
82100
while line > -1:
83101
act_line = lines[line]
84-
if act_line.rstrip().endswith('\\'):
102+
if (act_line.rstrip().endswith('\\') or
103+
act_line.rstrip().endswith('(') or
104+
act_line.rstrip().endswith(',')):
85105
act_lines.insert(0, act_line)
86106
line -= 1
107+
if act_line.rstrip().endswith('('):
108+
# Needs to be added to the end of the code before parsing
109+
# to make it valid, otherwise the node type could end
110+
# being an 'error_node' for multi-line imports that use '('
111+
last_character = ')'
87112
else:
88113
break
89-
tokens = parso.parse('\n'.join(act_lines).split(';')[-1].strip())
90-
return tokens.children[0].type not in _IMPORTS
114+
if '(' in act_lines[-1].strip():
115+
last_character = ')'
116+
code = '\n'.join(act_lines).split(';')[-1].strip() + last_character
117+
tokens = parso.parse(code)
118+
expr_type = tokens.children[0].type
119+
return (expr_type not in _IMPORTS and
120+
not (expr_type in _ERRORS and 'import' in code))
91121

92122

93123
def _format_completion(d, include_params=True):
@@ -100,19 +130,25 @@ def _format_completion(d, include_params=True):
100130
'insertText': d.name
101131
}
102132

103-
if include_params and hasattr(d, 'params') and d.params:
133+
if (include_params and hasattr(d, 'params') and d.params and
134+
not is_exception_class(d.name)):
104135
positional_args = [param for param in d.params if '=' not in param.description]
105136

106-
# For completions with params, we can generate a snippet instead
107-
completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
108-
snippet = d.name + '('
109-
for i, param in enumerate(positional_args):
110-
name = param.name if param.name != '/' else '\\/'
111-
snippet += '${%s:%s}' % (i + 1, name)
112-
if i < len(positional_args) - 1:
113-
snippet += ', '
114-
snippet += ')$0'
115-
completion['insertText'] = snippet
137+
if len(positional_args) > 1:
138+
# For completions with params, we can generate a snippet instead
139+
completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
140+
snippet = d.name + '('
141+
for i, param in enumerate(positional_args):
142+
name = param.name if param.name != '/' else '\\/'
143+
snippet += '${%s:%s}' % (i + 1, name)
144+
if i < len(positional_args) - 1:
145+
snippet += ', '
146+
snippet += ')$0'
147+
completion['insertText'] = snippet
148+
elif len(positional_args) == 1:
149+
completion['insertText'] = d.name + '($0)'
150+
else:
151+
completion['insertText'] = d.name + '()'
116152

117153
return completion
118154

test/plugins/test_completion.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,7 @@ def test_snippets_completion(config):
189189

190190
com_position = {'line': 1, 'character': len(doc_snippets)}
191191
completions = pyls_jedi_completions(config, doc, com_position)
192-
out = 'defaultdict(${1:kwargs})$0'
193-
assert completions[0]['insertText'] == out
192+
assert completions[0]['insertText'] == 'defaultdict($0)'
194193

195194

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

207206

207+
def test_multiline_import_snippets(config):
208+
document = 'from datetime import(\n date,\n datetime)\na=date'
209+
doc = Document(DOC_URI, document)
210+
config.capabilities['textDocument'] = {
211+
'completion': {'completionItem': {'snippetSupport': True}}}
212+
config.update({'plugins': {'jedi_completion': {'include_params': True}}})
213+
214+
position = {'line': 1, 'character': 5}
215+
completions = pyls_jedi_completions(config, doc, position)
216+
assert completions[0]['insertText'] == 'date'
217+
218+
position = {'line': 2, 'character': 9}
219+
completions = pyls_jedi_completions(config, doc, position)
220+
assert completions[0]['insertText'] == 'datetime'
221+
222+
208223
def test_multiline_snippets(config):
209224
document = 'from datetime import\\\n date,\\\n datetime \na=date'
210225
doc = Document(DOC_URI, document)

0 commit comments

Comments
 (0)