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

Make the auto-completion asynchronous #306

Merged
merged 9 commits into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 62 additions & 46 deletions sublime_jedi/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

import sublime
import sublime_plugin
from threading import Lock

from .console_logging import getLogger
from .daemon import ask_daemon, ask_daemon_with_timeout
from .utils import (get_settings,
is_python_scope,
is_repl,)
from .daemon import ask_daemon
from .utils import get_settings, is_python_scope, is_repl


logger = getLogger(__name__)
FOLLOWING_CHARS = set(["\r", "\n", "\t", " ", ")", "]", ";", "}", "\x00"])
Expand Down Expand Up @@ -115,9 +115,17 @@ def show_template(self, view, template):
class Autocomplete(sublime_plugin.ViewEventListener):
"""Sublime Text autocompletion integration."""

_lock = Lock()
_completions = []
_previous_completions = []
_last_location = None

def __enabled(self):
settings = get_settings(self.view)

if sublime.active_window().active_view().id() != self.view.id():
return False

if is_repl(self.view) and not settings['enable_in_sublime_repl']:
logger.debug("JEDI does not complete in SublimeREPL views.")
return False
Expand Down Expand Up @@ -146,61 +154,69 @@ def on_query_completions(self, prefix, locations):
logger.info('JEDI completion triggered.')

settings = get_settings(self.view)
only_jedi_completion = (
settings['sublime_completions_visibility'] in ('default', 'jedi')
)

previous_char = self.view.substr(locations[0] - 1)
if settings['only_complete_after_regex']:
previous_char = self.view.substr(locations[0] - 1)
if not re.match(settings['only_complete_after_regex'], previous_char):
return False

cplns = ask_daemon_with_timeout(
self.view,
'autocomplete',
location=locations[0],
timeout=settings['completion_timeout']
)

logger.info("Completion completed.")

cplns = [tuple(x) for x in self._sort_completions(cplns)]
logger.debug("Completions: {0}".format(cplns))

# disabled due to can't reproduce
# self._fix_tab_completion_issue()

if only_jedi_completion:
return cplns, PLUGIN_ONLY_COMPLETION
return cplns
with self._lock:
if self._last_location != locations[0]:
self._last_location = locations[0]
ask_daemon(
self.view,
self._receive_completions,
'autocomplete',
location=locations[0],
)
return [], PLUGIN_ONLY_COMPLETION

if self._last_location == locations[0] and self._completions:
self._last_location = None
return [
tuple(x)
for x in self._sort_completions(self._completions)
]

def _receive_completions(self, view, completions):
if not completions:
return

logger.debug("Completions: {0}".format(completions))

with self._lock:
self._previous_completions = self._completions
self._completions = completions

if (completions and
not view.is_auto_complete_visible() or
not self._is_completions_subset()):
only_jedi_completion = (
get_settings(self.view)['sublime_completions_visibility']
in ('default', 'jedi')
)
view.run_command('hide_auto_complete')
view.run_command('auto_complete', {
'api_completions_only': only_jedi_completion,
'disable_auto_insert': True,
'next_completion_if_showing': False,
})

def _sort_completions(self, completions):
"""Sort completions by frequency in document."""
buffer = self.view.substr(sublime.Region(0, self.view.size()))

return sorted(
completions,
key=lambda x: (
-buffer.count(x[1]), # frequency in the text
-buffer.count(x[1]),
len(x[1]) - len(x[1].strip('_')), # how many undescores
x[1] # alphabetically
)
)

def _fix_tab_completion_issue(self):
"""Fix issue with tab completion & commit on tab.

When you hit <tab> after completion commit,
completion pop-up will appears
and `\t`(tabulation) would be inserted
the fix detects such behavior and trying avoidt.
"""
logger.debug("command history: " + str([
self.view.command_history(-1),
self.view.command_history(0),
self.view.command_history(1),
]))

last_command = self.view.command_history(0)
if last_command == (u'insert', {'characters': u'\t'}, 1):
self.view.run_command('undo')
def _is_completions_subset(self):
with self._lock:
completions = set(
completion for _, completion in self._completions)
previous = set(
completion for _, completion in self._previous_completions)
return completions.issubset(previous)
32 changes: 1 addition & 31 deletions sublime_jedi/daemon.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from concurrent.futures import ThreadPoolExecutor

from functools import wraps
from collections import defaultdict
Expand Down Expand Up @@ -58,36 +58,6 @@ def ask_daemon_sync(view, ask_type, ask_kwargs, location=None):
*_prepare_request_data(view, location))


def ask_daemon_with_timeout(
view,
ask_type,
ask_kwargs=None,
location=None,
timeout=3):
"""Jedi sync request shortcut with timeout.

:type view: sublime.View
:type ask_type: str
:type ask_kwargs: dict or None
:type location: type of (int, int) or None
:type timeout: int
"""
daemon = _get_daemon(view)
requestor = _get_requestor(view)
request_data = _prepare_request_data(view, location)

def _target():
return daemon.request(ask_type, ask_kwargs or {}, *request_data)

request = requestor.submit(_target)
try:
return request.result(timeout=timeout)
except TimeoutError:
# no need to wait more
request.cancel()
raise


def ask_daemon(view, callback, ask_type, ask_kwargs=None, location=None):
"""Jedi async request shortcut.

Expand Down