Some work on completion for the editor

This commit is contained in:
Kovid Goyal 2014-12-18 18:43:03 +05:30
parent d102bc256c
commit b17ff26950
2 changed files with 187 additions and 1 deletions

View File

@ -0,0 +1,132 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
from threading import Event
from collections import namedtuple
from PyQt5.Qt import QObject, pyqtSignal, Qt
from calibre.ebooks.oeb.polish.container import OEB_STYLES, OEB_FONTS
from calibre.gui2 import is_gui_thread
from calibre.gui2.tweak_book import current_container
from calibre.utils.ipc import eintr_retry_call
from calibre.utils.matcher import Matcher
Request = namedtuple('Request', 'id type data query')
names_cache = frozenset()
def control(func):
func.function_type = 'control'
return func
def data(func):
func.function_type = 'data'
return func
@control
def clear_caches(cache_type, data_conn):
global names_cache
if cache_type is None or cache_type == 'names':
names_cache = frozenset()
@data
def names_data(request_data):
c = current_container()
return c.mime_map, {n for n, is_linear in c.spine_names}
class DataError(Exception):
def __init__(self, tb, msg=None):
Exception.__init__(self, msg or _('Failed to get completion data'))
self.tb = tb
def get_data(data_conn, data_type, data=None):
eintr_retry_call(data_conn.send, Request(None, data_type, data, None))
result, tb = eintr_retry_call(data_conn.recv)
if tb:
raise DataError(tb)
return result
class Name(unicode):
def __new__(self, name, mime_type, spine_names):
ans = unicode.__new__(self, name)
ans.mime_type = mime_type
ans.in_spine = name in spine_names
return ans
@control
def complete_names(names_type, data_conn):
global names_cache
if not names_cache:
mime_map, spine_names = get_data(data_conn, 'names_data')
names_cache = frozenset(Name(name, mt, spine_names) for name, mt in mime_map.iteritems())
ans = names_cache
if names_type == 'text_link':
ans = frozenset(n for n in names_cache if n.in_spine)
elif names_type == 'stylesheet':
ans = frozenset(n for n in names_cache if n.mime_type in OEB_STYLES)
elif names_type == 'image':
ans = frozenset(n for n in names_cache if n.mime_type.startswith('image/'))
elif names_type == 'font':
ans = frozenset(n for n in names_cache if n.mime_type in OEB_FONTS)
return ans, {}
_current_matcher = (None, None, None)
def handle_control_request(request, data_conn):
global _current_matcher
ans = control_funcs[request.type](request.data, data_conn)
if ans is not None:
items, matcher_kwargs = ans
fingerprint = hash(items)
if fingerprint != _current_matcher[0] or matcher_kwargs != _current_matcher[1]:
_current_matcher = (fingerprint, matcher_kwargs, Matcher(items, **matcher_kwargs))
ans = _current_matcher[-1](request.query or '', limit=50)
return ans
class HandleDataRequest(QObject):
# Ensure data is obtained in the GUI thread
call = pyqtSignal(object, object, object)
def __init__(self):
QObject.__init__(self)
self.called = Event()
self.call.connect(self.run_func, Qt.QueuedConnection)
def run_func(self, func, data):
try:
self.result, self.tb = func(data), None
except Exception:
import traceback
self.result, self.tb = None, traceback.format_exc()
finally:
self.called.set()
def __call__(self, request):
func = data_funcs[request.type]
if is_gui_thread():
try:
return func(request.data), None
except Exception:
import traceback
return None, traceback.format_exc()
self.called.clear()
self.call.emit(func, request.data)
self.called.wait()
try:
return self.result, self.tb
finally:
del self.result, self.tb
handle_data_request = HandleDataRequest()
control_funcs = {name:func for name, func in globals().iteritems() if getattr(func, 'function_type', None) == 'control'}
data_funcs = {name:func for name, func in globals().iteritems() if getattr(func, 'function_type', None) == 'data'}

View File

@ -10,6 +10,7 @@ import cPickle, os, sys
from threading import Thread, Event from threading import Thread, Event
from Queue import Queue from Queue import Queue
from contextlib import closing from contextlib import closing
from collections import namedtuple
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc import eintr_retry_call
@ -38,6 +39,9 @@ class CompletionWorker(Thread):
p.stdin.flush(), p.stdin.close() p.stdin.flush(), p.stdin.close()
self.control_conn = eintr_retry_call(self.listener.accept) self.control_conn = eintr_retry_call(self.listener.accept)
self.data_conn = eintr_retry_call(self.listener.accept) self.data_conn = eintr_retry_call(self.listener.accept)
self.data_thread = t = Thread(name='CWData', target=self.handle_data_requests)
t.daemon = True
t.start()
self.connected.set() self.connected.set()
def send(self, data, conn=None): def send(self, data, conn=None):
@ -59,6 +63,29 @@ class CompletionWorker(Thread):
def wait_for_connection(self, timeout=None): def wait_for_connection(self, timeout=None):
self.connected.wait(timeout) self.connected.wait(timeout)
def handle_data_requests(self):
from calibre.gui2.tweak_book.completion.basic import handle_data_request
while True:
try:
req = self.recv(self.data_conn)
except EOFError:
break
except Exception:
import traceback
traceback.print_exc()
break
if req is None or self.shutting_down:
break
result, tb = handle_data_request(req)
try:
self.send((result, tb), self.data_conn)
except EOFError:
break
except Exception:
import traceback
traceback.print_exc()
break
def run(self): def run(self):
self.launch_worker_process() self.launch_worker_process()
while True: while True:
@ -69,8 +96,13 @@ class CompletionWorker(Thread):
def shutdown(self): def shutdown(self):
self.shutting_down = True self.shutting_down = True
self.main_queue.put(None) self.main_queue.put(None)
for conn in (self.control_conn, self.data_conn):
try:
conn.close()
except Exception:
pass
p = self.worker_process p = self.worker_process
if p.returncode is None: if p.poll() is None:
self.worker_process.terminate() self.worker_process.terminate()
t = self.reap_thread = Thread(target=p.wait) t = self.reap_thread = Thread(target=p.wait)
t.daemon = True t.daemon = True
@ -96,6 +128,28 @@ def run_main(func):
with closing(Client(address, authkey=key)) as control_conn, closing(Client(address, authkey=key)) as data_conn: with closing(Client(address, authkey=key)) as control_conn, closing(Client(address, authkey=key)) as data_conn:
func(control_conn, data_conn) func(control_conn, data_conn)
Result = namedtuple('Result', 'request_id ans traceback')
def main(control_conn, data_conn):
from calibre.gui2.tweak_book.completion.basic import handle_control_request
while True:
try:
request = eintr_retry_call(control_conn.recv)
except EOFError:
break
if request is None:
break
try:
ans, tb = handle_control_request(request, data_conn), None
except Exception:
import traceback
ans, tb = None, traceback.format_exc()
result = Result(request.id, ans, tb)
try:
eintr_retry_call(control_conn.send, result)
except EOFError:
break
def test_main(control_conn, data_conn): def test_main(control_conn, data_conn):
obj = control_conn.recv() obj = control_conn.recv()
control_conn.send(obj) control_conn.send(obj)