Skip to content

Commit 7ddf562

Browse files
authored
Feature/completion item/resolve (#25)
* Implement completionItem/resolve * Fix CI-only test for resolve * Address pylint notes, fix names * Only sent the first result for completionItem/resolve * Fix indentation * Finalise pyls -> pylsp transition * Document the `eager` option * Add a TODO note * Use document-specific cache proposed in the review The document identifier is stored in the completion item data. * Add data.doc_uri for rope too
1 parent 87b76d7 commit 7ddf562

File tree

8 files changed

+125
-19
lines changed

8 files changed

+125
-19
lines changed

CONFIGURATION.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E
1111
| `pylsp.plugins.jedi_completion.include_params` | `boolean` | Auto-completes methods and classes with tabstops for each parameter. | `true` |
1212
| `pylsp.plugins.jedi_completion.include_class_objects` | `boolean` | Adds class objects as a separate completion item. | `true` |
1313
| `pylsp.plugins.jedi_completion.fuzzy` | `boolean` | Enable fuzzy when requesting autocomplete. | `false` |
14+
| `pylsp.plugins.jedi_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` |
1415
| `pylsp.plugins.jedi_definition.enabled` | `boolean` | Enable or disable the plugin. | `true` |
1516
| `pylsp.plugins.jedi_definition.follow_imports` | `boolean` | The goto call will follow imports. | `true` |
1617
| `pylsp.plugins.jedi_definition.follow_builtin_imports` | `boolean` | If follow_imports is True will decide if it follow builtin imports. | `true` |

pylsp/config/schema.json

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
"default": false,
5050
"description": "Enable fuzzy when requesting autocomplete."
5151
},
52+
"pylsp.plugins.jedi_completion.eager": {
53+
"type": "boolean",
54+
"default": false,
55+
"description": "Resolve documentation and detail eagerly."
56+
},
5257
"pylsp.plugins.jedi_definition.enabled": {
5358
"type": "boolean",
5459
"default": true,

pylsp/hookspecs.py

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def pylsp_completions(config, workspace, document, position):
2929
pass
3030

3131

32+
@hookspec(firstresult=True)
33+
def pylsp_completion_item_resolve(config, workspace, document, completion_item):
34+
pass
35+
36+
3237
@hookspec
3338
def pylsp_definitions(config, workspace, document, position):
3439
pass

pylsp/plugins/jedi_completion.py

+35-5
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@
3737
@hookimpl
3838
def pylsp_completions(config, document, position):
3939
"""Get formatted completions for current code position"""
40+
# pylint: disable=too-many-locals
41+
4042
settings = config.plugin_settings('jedi_completion', document_path=document.path)
43+
resolve_eagerly = settings.get('eager', False)
4144
code_position = _utils.position_to_jedi_linecolumn(document, position)
4245

43-
code_position["fuzzy"] = settings.get("fuzzy", False)
46+
code_position['fuzzy'] = settings.get('fuzzy', False)
4447
completions = document.jedi_script(use_document_path=True).complete(**code_position)
4548

4649
if not completions:
@@ -60,17 +63,37 @@ def pylsp_completions(config, document, position):
6063
for c in completions
6164
]
6265

66+
# TODO split up once other improvements are merged
6367
if include_class_objects:
6468
for c in completions:
6569
if c.type == 'class':
66-
completion_dict = _format_completion(c, False)
70+
completion_dict = _format_completion(c, False, resolve=resolve_eagerly)
6771
completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter
6872
completion_dict['label'] += ' object'
6973
ready_completions.append(completion_dict)
7074

75+
for completion_dict in ready_completions:
76+
completion_dict['data'] = {
77+
'doc_uri': document.uri
78+
}
79+
80+
# most recently retrieved completion items, used for resolution
81+
document.shared_data['LAST_JEDI_COMPLETIONS'] = {
82+
# label is the only required property; here it is assumed to be unique
83+
completion['label']: (completion, data)
84+
for completion, data in zip(ready_completions, completions)
85+
}
86+
7187
return ready_completions or None
7288

7389

90+
@hookimpl
91+
def pylsp_completion_item_resolve(completion_item, document):
92+
"""Resolve formatted completion for given non-resolved completion"""
93+
completion, data = document.shared_data['LAST_JEDI_COMPLETIONS'].get(completion_item['label'])
94+
return _resolve_completion(completion, data)
95+
96+
7497
def is_exception_class(name):
7598
"""
7699
Determine if a class name is an instance of an Exception.
@@ -121,16 +144,23 @@ def use_snippets(document, position):
121144
not (expr_type in _ERRORS and 'import' in code))
122145

123146

124-
def _format_completion(d, include_params=True):
147+
def _resolve_completion(completion, d):
148+
completion['detail'] = _detail(d)
149+
completion['documentation'] = _utils.format_docstring(d.docstring())
150+
return completion
151+
152+
153+
def _format_completion(d, include_params=True, resolve=False):
125154
completion = {
126155
'label': _label(d),
127156
'kind': _TYPE_MAP.get(d.type),
128-
'detail': _detail(d),
129-
'documentation': _utils.format_docstring(d.docstring()),
130157
'sortText': _sort_text(d),
131158
'insertText': d.name
132159
}
133160

161+
if resolve:
162+
completion = _resolve_completion(completion, d)
163+
134164
if d.type == 'path':
135165
path = osp.normpath(d.name)
136166
path = path.replace('\\', '\\\\')

pylsp/plugins/rope_completion.py

+41-11
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,26 @@
1313
@hookimpl
1414
def pylsp_settings():
1515
# Default rope_completion to disabled
16-
return {'plugins': {'rope_completion': {'enabled': False}}}
16+
return {'plugins': {'rope_completion': {'enabled': False, 'eager': False}}}
17+
18+
19+
def _resolve_completion(completion, data):
20+
try:
21+
doc = data.get_doc()
22+
except AttributeError:
23+
doc = ""
24+
completion['detail'] = '{0} {1}'.format(data.scope or "", data.name)
25+
completion['documentation'] = doc
26+
return completion
1727

1828

1929
@hookimpl
2030
def pylsp_completions(config, workspace, document, position):
31+
# pylint: disable=too-many-locals
32+
33+
settings = config.plugin_settings('rope_completion', document_path=document.path)
34+
resolve_eagerly = settings.get('eager', False)
35+
2136
# Rope is a bit rubbish at completing module imports, so we'll return None
2237
word = document.word_at_position({
2338
# The -1 should really be trying to look at the previous word, but that might be quite expensive
@@ -41,22 +56,37 @@ def pylsp_completions(config, workspace, document, position):
4156
definitions = sorted_proposals(definitions)
4257
new_definitions = []
4358
for d in definitions:
44-
try:
45-
doc = d.get_doc()
46-
except AttributeError:
47-
doc = None
48-
new_definitions.append({
59+
item = {
4960
'label': d.name,
5061
'kind': _kind(d),
51-
'detail': '{0} {1}'.format(d.scope or "", d.name),
52-
'documentation': doc or "",
53-
'sortText': _sort_text(d)
54-
})
62+
'sortText': _sort_text(d),
63+
'data': {
64+
'doc_uri': document.uri
65+
}
66+
}
67+
if resolve_eagerly:
68+
item = _resolve_completion(item, d)
69+
new_definitions.append(item)
70+
71+
# most recently retrieved completion items, used for resolution
72+
document.shared_data['LAST_ROPE_COMPLETIONS'] = {
73+
# label is the only required property; here it is assumed to be unique
74+
completion['label']: (completion, data)
75+
for completion, data in zip(new_definitions, definitions)
76+
}
77+
5578
definitions = new_definitions
5679

5780
return definitions or None
5881

5982

83+
@hookimpl
84+
def pylsp_completion_item_resolve(completion_item, document):
85+
"""Resolve formatted completion for given non-resolved completion"""
86+
completion, data = document.shared_data['LAST_ROPE_COMPLETIONS'].get(completion_item['label'])
87+
return _resolve_completion(completion, data)
88+
89+
6090
def _sort_text(definition):
6191
""" Ensure builtins appear at the bottom.
6292
Description is of format <type>: <module>.<item>
@@ -72,7 +102,7 @@ def _sort_text(definition):
72102

73103

74104
def _kind(d):
75-
""" Return the VSCode type """
105+
""" Return the LSP type """
76106
MAP = {
77107
'none': lsp.CompletionItemKind.Value,
78108
'type': lsp.CompletionItemKind.Class,

pylsp/python_lsp.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ def capabilities(self):
162162
'resolveProvider': False, # We may need to make this configurable
163163
},
164164
'completionProvider': {
165-
'resolveProvider': False, # We know everything ahead of time
166-
'triggerCharacters': ['.']
165+
'resolveProvider': True, # We could know everything ahead of time, but this takes time to transfer
166+
'triggerCharacters': ['.'],
167167
},
168168
'documentFormattingProvider': True,
169169
'documentHighlightProvider': True,
@@ -250,6 +250,10 @@ def completions(self, doc_uri, position):
250250
'items': flatten(completions)
251251
}
252252

253+
def completion_item_resolve(self, completion_item):
254+
doc_uri = completion_item.get('data', {}).get('doc_uri', None)
255+
return self._hook('pylsp_completion_item_resolve', doc_uri, completion_item=completion_item)
256+
253257
def definitions(self, doc_uri, position):
254258
return flatten(self._hook('pylsp_definitions', doc_uri, position=position))
255259

@@ -296,6 +300,9 @@ def signature_help(self, doc_uri, position):
296300
def folding(self, doc_uri):
297301
return flatten(self._hook('pylsp_folding_range', doc_uri))
298302

303+
def m_completion_item__resolve(self, **completionItem):
304+
return self.completion_item_resolve(completionItem)
305+
299306
def m_text_document__did_close(self, textDocument=None, **_kwargs):
300307
workspace = self._match_uri_to_workspace(textDocument['uri'])
301308
workspace.rm_document(textDocument['uri'])

pylsp/workspace.py

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ def __init__(self, uri, workspace, source=None, version=None, local=True, extra_
138138
self.path = uris.to_fs_path(uri)
139139
self.dot_path = _utils.path_to_dot_name(self.path)
140140
self.filename = os.path.basename(self.path)
141+
self.shared_data = {}
141142

142143
self._config = workspace._config
143144
self._workspace = workspace

test/plugins/test_completion.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pylsp import uris, lsp
1313
from pylsp.workspace import Document
1414
from pylsp.plugins.jedi_completion import pylsp_completions as pylsp_jedi_completions
15+
from pylsp.plugins.jedi_completion import pylsp_completion_item_resolve as pylsp_jedi_completion_item_resolve
1516
from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions
1617
from pylsp._utils import JEDI_VERSION
1718

@@ -44,6 +45,10 @@ def everyone(self, a, b, c=None, d=2):
4445
print Hello().world
4546
4647
print Hello().every
48+
49+
def documented_hello():
50+
\"\"\"Sends a polite greeting\"\"\"
51+
pass
4752
"""
4853

4954

@@ -139,6 +144,26 @@ def test_jedi_completion(config, workspace):
139144
pylsp_jedi_completions(config, doc, {'line': 1, 'character': 1000})
140145

141146

147+
def test_jedi_completion_item_resolve(config, workspace):
148+
# Over the blank line
149+
com_position = {'line': 8, 'character': 0}
150+
doc = Document(DOC_URI, workspace, DOC)
151+
completions = pylsp_jedi_completions(config, doc, com_position)
152+
153+
items = {c['label']: c for c in completions}
154+
155+
documented_hello_item = items['documented_hello()']
156+
157+
assert 'documentation' not in documented_hello_item
158+
assert 'detail' not in documented_hello_item
159+
160+
resolved_documented_hello = pylsp_jedi_completion_item_resolve(
161+
completion_item=documented_hello_item,
162+
document=doc
163+
)
164+
assert 'Sends a polite greeting' in resolved_documented_hello['documentation']
165+
166+
142167
def test_jedi_completion_with_fuzzy_enabled(config, workspace):
143168
# Over 'i' in os.path.isabs(...)
144169
config.update({'plugins': {'jedi_completion': {'fuzzy': True}}})
@@ -410,7 +435,9 @@ def test_jedi_completion_environment(workspace):
410435
# After 'import logh' with new environment
411436
completions = pylsp_jedi_completions(doc._config, doc, com_position)
412437
assert completions[0]['label'] == 'loghub'
413-
assert 'changelog generator' in completions[0]['documentation'].lower()
438+
439+
resolved = pylsp_jedi_completion_item_resolve(completions[0], doc)
440+
assert 'changelog generator' in resolved['documentation'].lower()
414441

415442

416443
def test_document_path_completions(tmpdir, workspace_other_root_path):

0 commit comments

Comments
 (0)