diff --git a/pyls/hookspecs.py b/pyls/hookspecs.py index 5f30cd08..ec9c6675 100644 --- a/pyls/hookspecs.py +++ b/pyls/hookspecs.py @@ -27,6 +27,11 @@ def pyls_completions(config, workspace, document, position): pass +@hookspec(firstresult=True) +def pyls_completion_item_resolve(config, workspace, completion_item): + pass + + @hookspec def pyls_definitions(config, workspace, document, position): pass diff --git a/pyls/plugins/jedi_completion.py b/pyls/plugins/jedi_completion.py index ff9254a0..978d0bd8 100644 --- a/pyls/plugins/jedi_completion.py +++ b/pyls/plugins/jedi_completion.py @@ -49,11 +49,19 @@ # Types of parso node for errors _ERRORS = ('error_node', ) +# most recently retrieved completion items, used for resolution +_LAST_COMPLETIONS = {} + @hookimpl def pyls_completions(config, document, position): """Get formatted completions for current code position""" + # pylint: disable=too-many-locals + # pylint: disable=global-statement + global _LAST_COMPLETIONS + settings = config.plugin_settings('jedi_completion', document_path=document.path) + resolve_eagerly = settings.get('eager', False) code_position = _utils.position_to_jedi_linecolumn(document, position) code_position["fuzzy"] = settings.get("fuzzy", False) @@ -79,14 +87,27 @@ def pyls_completions(config, document, position): if include_class_objects: for c in completions: if c.type == 'class': - completion_dict = _format_completion(c, False) + completion_dict = _format_completion(c, False, resolve=resolve_eagerly) completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter completion_dict['label'] += ' object' ready_completions.append(completion_dict) + _LAST_COMPLETIONS = { + # label is the only required property; here it is assumed to be unique + completion['label']: (completion, data) + for completion, data in zip(ready_completions, completions) + } + return ready_completions or None +@hookimpl +def pyls_completion_item_resolve(completion_item): + """Resolve formatted completion for given non-resolved completion""" + completion, data = _LAST_COMPLETIONS.get(completion_item['label']) + return _resolve_completion(completion, data) + + def is_exception_class(name): """ Determine if a class name is an instance of an Exception. @@ -137,16 +158,23 @@ def use_snippets(document, position): not (expr_type in _ERRORS and 'import' in code)) -def _format_completion(d, include_params=True): +def _resolve_completion(completion, d): + completion['detail'] = _detail(d) + completion['documentation'] = _utils.format_docstring(d.docstring()) + return completion + + +def _format_completion(d, include_params=True, resolve=False): completion = { 'label': _label(d), 'kind': _TYPE_MAP.get(d.type), - 'detail': _detail(d), - 'documentation': _utils.format_docstring(d.docstring()), 'sortText': _sort_text(d), 'insertText': d.name } + if resolve: + completion = _resolve_completion(completion, d) + if d.type == 'path': path = osp.normpath(d.name) path = path.replace('\\', '\\\\') diff --git a/pyls/plugins/rope_completion.py b/pyls/plugins/rope_completion.py index e556e464..e5b07be6 100644 --- a/pyls/plugins/rope_completion.py +++ b/pyls/plugins/rope_completion.py @@ -7,15 +7,35 @@ log = logging.getLogger(__name__) +# most recently retrieved completion items, used for resolution +_LAST_COMPLETIONS = {} + @hookimpl def pyls_settings(): # Default rope_completion to disabled - return {'plugins': {'rope_completion': {'enabled': False}}} + return {'plugins': {'rope_completion': {'enabled': False, 'eager': False}}} + + +def _resolve_completion(completion, data): + try: + doc = data.get_doc() + except AttributeError: + doc = "" + completion['detail'] = '{0} {1}'.format(data.scope or "", data.name) + completion['documentation'] = doc + return completion @hookimpl def pyls_completions(config, workspace, document, position): + # pylint: disable=too-many-locals + # pylint: disable=global-statement + global _LAST_COMPLETIONS + + settings = config.plugin_settings('rope_completion', document_path=document.path) + resolve_eagerly = settings.get('eager', False) + # Rope is a bit rubbish at completing module imports, so we'll return None word = document.word_at_position({ # The -1 should really be trying to look at the previous word, but that might be quite expensive @@ -39,22 +59,33 @@ def pyls_completions(config, workspace, document, position): definitions = sorted_proposals(definitions) new_definitions = [] for d in definitions: - try: - doc = d.get_doc() - except AttributeError: - doc = None - new_definitions.append({ + item = { 'label': d.name, 'kind': _kind(d), - 'detail': '{0} {1}'.format(d.scope or "", d.name), - 'documentation': doc or "", 'sortText': _sort_text(d) - }) + } + if resolve_eagerly: + item = _resolve_completion(item, d) + new_definitions.append(item) + + _LAST_COMPLETIONS = { + # label is the only required property; here it is assumed to be unique + completion['label']: (completion, data) + for completion, data in zip(new_definitions, definitions) + } + definitions = new_definitions return definitions or None +@hookimpl +def pyls_completion_item_resolve(completion_item): + """Resolve formatted completion for given non-resolved completion""" + completion, data = _LAST_COMPLETIONS.get(completion_item['label']) + return _resolve_completion(completion, data) + + def _sort_text(definition): """ Ensure builtins appear at the bottom. Description is of format : . @@ -70,7 +101,7 @@ def _sort_text(definition): def _kind(d): - """ Return the VSCode type """ + """ Return the LSP type """ MAP = { 'none': lsp.CompletionItemKind.Value, 'type': lsp.CompletionItemKind.Class, diff --git a/pyls/python_ls.py b/pyls/python_ls.py index 0a11aa9b..d84989a4 100644 --- a/pyls/python_ls.py +++ b/pyls/python_ls.py @@ -162,8 +162,8 @@ def capabilities(self): 'resolveProvider': False, # We may need to make this configurable }, 'completionProvider': { - 'resolveProvider': False, # We know everything ahead of time - 'triggerCharacters': ['.'] + 'resolveProvider': True, # We could know everything ahead of time, but this takes time to transfer + 'triggerCharacters': ['.'], }, 'documentFormattingProvider': True, 'documentHighlightProvider': True, @@ -243,6 +243,9 @@ def completions(self, doc_uri, position): 'items': flatten(completions) } + def completion_item_resolve(self, completion_item): + return self._hook('pyls_completion_item_resolve', completion_item=completion_item) + def definitions(self, doc_uri, position): return flatten(self._hook('pyls_definitions', doc_uri, position=position)) @@ -289,6 +292,9 @@ def signature_help(self, doc_uri, position): def folding(self, doc_uri): return flatten(self._hook('pyls_folding_range', doc_uri)) + def m_completion_item__resolve(self, **completionItem): + return self.completion_item_resolve(completionItem) + def m_text_document__did_close(self, textDocument=None, **_kwargs): workspace = self._match_uri_to_workspace(textDocument['uri']) workspace.rm_document(textDocument['uri']) diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py index 247c2c23..d75a4b55 100644 --- a/test/plugins/test_completion.py +++ b/test/plugins/test_completion.py @@ -7,6 +7,7 @@ from pyls import uris, lsp from pyls.workspace import Document from pyls.plugins.jedi_completion import pyls_completions as pyls_jedi_completions +from pyls.plugins.jedi_completion import pyls_completion_item_resolve as pyls_jedi_completion_item_resolve from pyls.plugins.rope_completion import pyls_completions as pyls_rope_completions @@ -38,6 +39,10 @@ def everyone(self, a, b, c=None, d=2): print Hello().world print Hello().every + +def documented_hello(): + \"\"\"Sends a polite greeting\"\"\" + pass """ @@ -62,6 +67,25 @@ def test_jedi_completion(config, workspace): pyls_jedi_completions(config, doc, {'line': 1, 'character': 1000}) +def test_jedi_completion_item_resolve(config, workspace): + # Over the blank line + com_position = {'line': 8, 'character': 0} + doc = Document(DOC_URI, workspace, DOC) + completions = pyls_jedi_completions(config, doc, com_position) + + items = {c['label']: c for c in completions} + + documented_hello_item = items['documented_hello()'] + + assert 'documentation' not in documented_hello_item + assert 'detail' not in documented_hello_item + + resolved_documented_hello = pyls_jedi_completion_item_resolve( + completion_item=documented_hello_item + ) + assert 'Sends a polite greeting' in resolved_documented_hello['documentation'] + + def test_jedi_completion_with_fuzzy_enabled(config, workspace): # Over 'i' in os.path.isabs(...) config.update({'plugins': {'jedi_completion': {'fuzzy': True}}}) @@ -333,7 +357,9 @@ def test_jedi_completion_environment(workspace): # After 'import logh' with new environment completions = pyls_jedi_completions(doc._config, doc, com_position) assert completions[0]['label'] == 'loghub' - assert 'changelog generator' in completions[0]['documentation'].lower() + + resolved = pyls_jedi_completion_item_resolve(completions[0]) + assert 'changelog generator' in resolved['documentation'].lower() def test_document_path_completions(tmpdir, workspace_other_root_path): diff --git a/vscode-client/package.json b/vscode-client/package.json index f28437ca..20b29c65 100644 --- a/vscode-client/package.json +++ b/vscode-client/package.json @@ -70,6 +70,11 @@ "default": false, "description": "Enable fuzzy when requesting autocomplete." }, + "pyls.plugins.jedi_completion.eager": { + "type": "boolean", + "default": false, + "description": "Resolve documentation and detail eagerly." + }, "pyls.plugins.jedi_definition.enabled": { "type": "boolean", "default": true, @@ -274,6 +279,11 @@ "default": true, "description": "Enable or disable the plugin." }, + "pyls.plugins.rope_completion.eager": { + "type": "boolean", + "default": false, + "description": "Resolve documentation and detail eagerly." + }, "pyls.plugins.yapf.enabled": { "type": "boolean", "default": true,