Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable full completions #47

Merged
merged 7 commits into from
Aug 28, 2017
Merged

Enable full completions #47

merged 7 commits into from
Aug 28, 2017

Conversation

tomv564
Copy link
Contributor

@tomv564 tomv564 commented Aug 25, 2017

For issue #25, where all completion requests (even manual) were ignored unless the cursor followed a trigger character.

The following improvements were made:

  • No more filtering on trigger chars (ctrl+space no longer ignored)
  • Switch to ViewEventListener (easier to disable, less risk with shared state)
  • Track completion states so we can discard and request new completions as the user keeps typing

To do:

  • verify only_complete_on_trigger_characters and make it a setting
  • also disable listener if server has no completionProvider
  • clean up debug logging
  • rename to CompletionHandler

main.py Outdated
return
@classmethod
def is_applicable(cls, settings):
syntax = settings.get('syntax')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In rare cases syntax may be None (annoying ST bug), check for this just in case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the behaviour would be as expected, but I'll add this fix anyways.
Is this documented somewhere?

main.py Outdated
def is_after_trigger_character(self, location):
if location > 0:
prev_char = self.view.substr(
sublime.Region(location - 1, location))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use sublime.Region(location - 1) I believe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can even call self.view.substr(location - 1) as substr understands both a Region and a Point

main.py Outdated
dict) else response
names = list(item.get('label') for item in items)
debug('got completions', names)
self.completions = list(self.format_completion(item) for item in items)
Copy link
Member

@rwols rwols Aug 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you be doing anything with the sortText property of the CompletionItem or will you let ST sort the results? The clangd server uses sortText to sort completions by priority, so you'll see properties and methods first, base classes somewhere middle, keywords last.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, is there a way to control what ST does with the completions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No there is not. ST merges and sorts all items it gets internally.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not immediately, it just presents that list as-is if the user hasn't typed more characters yet, so there might be some benefit to using sortText.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it could slow down LSP a lot if there are tons of completion items, you'll have to experiment with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rwols good to know!

main.py Outdated
def __init__(self):
self.completions = [] # type: List[Tuple[str, str]]
self.refreshing = False
class CompletionState(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some trouble seeing "old" completions when I typed fast, I hope this class fixes that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From some quick tests using the changes in this PR I don't see this problem anymore, nice job!

main.py Outdated
@@ -1605,7 +1605,7 @@ def format_completion(self, item) -> 'Tuple[str, str]':
insertText = item.get("insertText")
if insertText[0] == '$': # sublime needs leading '$' escaped.
insertText = '\$' + insertText[1:]
return ["{}\t{}".format(label, detail), insertText]
return "{}\t{}".format(label, detail), insertText
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, whether it's to be handled by LSP or the pyls, but detail returns def function for a function completion, which makes completions look a bit too verbose:

function def function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be something for pyls to look at.

@tomv564 tomv564 force-pushed the enable-full-completions branch from 16decfb to e0e7a38 Compare August 26, 2017 19:25
@tomv564
Copy link
Contributor Author

tomv564 commented Aug 26, 2017

Is anyone interested in giving this branch a test? I'm not 100% convinced it's working correctly.

Example: sublime still shows for snippet completion sometimes when you type f, instead of pyls's completions containing from

@rwols
Copy link
Member

rwols commented Aug 26, 2017

Is anyone interested in giving this branch a test?

Yes, I'm gonna give this a spin.

EDIT: I don't see any problems at first glance (tested with clangd)

main.py Outdated
sublime.Region(locations[0] - 1, locations[0]))
if prev_char not in autocomplete_triggers:
return None
elif self.state == CompletionState.REQUESTING or self.state == CompletionState.CANCELLING:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about: elif self.state in (CompletionState.REQUESTING, CompletionState.CANCELLING)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Seems quite easy to get the state machine stalled in CANCELLING state, once a language-server does not respond. From this point I'll never get any completion any more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice code suggestion. Why does the language server not respond?

@deathaxe
Copy link
Contributor

Furthermore there seems to be a general issue with requesting completions. Sometimes a race condition between the onChange change notification and the completion request occures. If onChange is sent later then the completion response, python-language-server returns ArgumentError as it does not yet know about the content of the edited line.

On the other hand the onChange triggers diagnostics scan and causes python-language-server to delay completions as it is busy with linting. Especially mypy plugin causes heavy delays.

@tomv564
Copy link
Contributor Author

tomv564 commented Aug 27, 2017

@deathaxe thanks for testing!
I too have seen pyls complain about some index being out of range. didChange is always called just before requesting completions, in what case does the language server receive it after the completions request?

In a discussion about the javascript-typescript server's completion performance the idea of processing diagnostics from a queue after a small delay was raised. This is worth proposing for pyls as well.

@deathaxe
Copy link
Contributor

I didn't investigate too much, but I think the race condition is related to view.on_modified_async()' handler, which is used to push changes to the language server, while the view.on_query_completions()is a synchronous event. Therefore in some situations the async on_modified handler might be delayed by other operations taking place in the background. So at the point of time, the completion is requested, the queue you try to purge might still be empty. I'd suggest to use synchronous handlers inDocumentSyncListener` as each event handler doesn't take any time to be processed. Some of the operations to read the view content might even take longer due to inter process communication.

the idea of processing diagnostics from a queue after a small delay was raised

I hate delays. They are caused by processing anyway. But creating a multithread or multiprocess architecture with a pool of worker threads/processes might be a good idea for pyls to process multiple requests/notifications in parallel. This might help much as pyls may already be busy with multiple linters run after each other while a completion request is received at the moment.

This requires to categorize the received requests/notifications some how. A new didChange request would need to be queued, but not processed until all pending operations on the last version of the document have finished. A new request such as a completion or signatureHelp might be added to the queue and processed immediately to respond as fast as possible, while diagnostics may still be in process.

This requires at least one dedicated thread/process to handle the synchronization events such as didOpen/didChange/... and one dedicated thread/process to handle requests. A new didChange might would then be queued and being processed as soon as the running requests queue is empty.

@tomv564 tomv564 merged commit ec7c17a into master Aug 28, 2017
@deathaxe
Copy link
Contributor

here's a little implementation to get a reference how quick jedi really is without LSP overhead.

# -*- encoding: utf-8 -*-
import os
import sys

import sublime
import sublime_plugin


sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib'))
from jedi.api import Script
sys.path.pop(0)


def format_completion(complete):
    """ Returns a tuple of the string that would be visible in
    the completion dialogue and the completion word

    :type complete: jedi.api_classes.Completion
    :rtype: (str, str)
    """
    display, insert = complete.name + '\t' + complete.type, complete.name
    return display, insert


class EventListener(sublime_plugin.EventListener):

    def on_load_async(self, view):
        # just load to trigger interpreter and create cache
        self._script(view, 0)

    def on_query_completions(self, view, prefix, locations):

        point = locations[0]
        if not view.match_selector(point, "source.python"):
            return None

        script = self._script(view, point)
        completions = script.completions()
        return (
            [format_completion(complete) for complete in completions],
            sublime.INHIBIT_WORD_COMPLETIONS
        )

    def _script(self, view, point):
        path = view.file_name() or ''
        region = sublime.Region(0, view.size())
        source = view.substr(region)
        line, column = view.rowcol(point)
        return Script(source, line + 1, column, path=path)

@tomv564 tomv564 mentioned this pull request Aug 28, 2017
4 tasks
@tomv564 tomv564 deleted the enable-full-completions branch October 14, 2017 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants