Move completion logic from statusbar widget to completer.

Fixes #247.
This commit is contained in:
Florian Bruhin 2014-11-10 07:49:22 +01:00
parent 5e7ef5201c
commit d71147898b
4 changed files with 144 additions and 180 deletions

View File

@ -19,10 +19,10 @@
"""Completer attached to a CompletionView."""
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QTimer
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config, configdata
from qutebrowser.commands import cmdutils
from qutebrowser.commands import cmdutils, runners
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.models import completion as models
from qutebrowser.models.completionfilter import CompletionFilterModel as CFM
@ -33,29 +33,22 @@ class Completer(QObject):
"""Completer which manages completions in a CompletionView.
Attributes:
_ignore_change: Whether to ignore the next completion update.
models: dict of available completion models.
_cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
_timer: The timer used to trigger the completion update.
_prefix: The prefix to be used for the next completion update.
_parts: The parts to be used for the next completion update.
_cursor_part: The cursor part index for the next completion update.
Signals:
change_completed_part: Text which should be substituted for the word
we're currently completing.
arg 0: The text to change to.
arg 1: True if the text should be set
immediately, without continuing
completing the current field.
"""
change_completed_part = pyqtSignal(str, bool)
def __init__(self, win_id, parent=None):
def __init__(self, cmd, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._cmd = cmd
self._cmd.update_completion.connect(self.schedule_completion_update)
self._cmd.textEdited.connect(self.on_text_edited)
self._ignore_change = False
self._empty_item_idx = None
self._models = {
usertypes.Completion.option: {},
@ -68,8 +61,6 @@ class Completer(QObject):
self._timer.setSingleShot(True)
self._timer.setInterval(0)
self._timer.timeout.connect(self.update_completion)
self._prefix = None
self._parts = None
self._cursor_part = None
def __repr__(self):
@ -224,7 +215,7 @@ class Completer(QObject):
return s
def selection_changed(self, selected, _deselected):
"""Emit change_completed_part if a new item was selected.
"""Change the completed part if a new item was selected.
Called from the views selectionChanged method.
@ -243,39 +234,30 @@ class Completer(QObject):
if model.count() == 1 and config.get('completion', 'quick-complete'):
# If we only have one item, we want to apply it immediately
# and go on to the next part.
self.change_completed_part.emit(data, True)
self.change_completed_part(data, immediate=True)
else:
self._ignore_change = True
self.change_completed_part.emit(data, False)
self.change_completed_part(data)
@pyqtSlot(str, list, int)
def on_update_completion(self, prefix, parts, cursor_part):
@pyqtSlot()
def schedule_completion_update(self):
"""Schedule updating/enabling completion.
Slot for the textChanged signal of the statusbar command widget.
For performance reasons we don't want to block here, instead we do this
in the background.
"""
log.completion.debug("Scheduling completion update.")
self._timer.start()
log.completion.debug("Scheduling completion update. prefix {}, parts "
"{}, cursor_part {}".format(prefix, parts,
cursor_part))
self._prefix = prefix
self._parts = parts
self._cursor_part = cursor_part
@pyqtSlot()
def update_completion(self):
"""Check if completions are available and activate them."""
self.update_cursor_part()
parts = self.split()
assert self._prefix is not None
assert self._parts is not None
assert self._cursor_part is not None
log.completion.debug("Updating completion - prefix {}, parts {}, "
"cursor_part {}".format(self._prefix, self._parts,
self._cursor_part))
log.completion.debug(
"Updating completion - prefix {}, parts {}, cursor_part {}".format(
self._cmd.prefix(), parts, self._cursor_part))
if self._ignore_change:
self._ignore_change = False
log.completion.debug("Ignoring completion update")
@ -284,7 +266,7 @@ class Completer(QObject):
completion = objreg.get('completion', scope='window',
window=self._win_id)
if self._prefix != ':':
if self._cmd.prefix() != ':':
# This is a search or gibberish, so we don't need to complete
# anything (yet)
# FIXME complete searchs
@ -292,7 +274,7 @@ class Completer(QObject):
completion.hide()
return
model = self._get_new_completion(self._parts, self._cursor_part)
model = self._get_new_completion(parts, self._cursor_part)
if model != self._model():
if model is None:
@ -301,19 +283,18 @@ class Completer(QObject):
completion.set_model(model)
if model is None:
log.completion.debug("No completion model for {}.".format(
self._parts))
log.completion.debug("No completion model for {}.".format(parts))
return
try:
pattern = self._parts[self._cursor_part].strip()
pattern = parts[self._cursor_part].strip()
except IndexError:
pattern = ''
self._model().set_pattern(pattern)
log.completion.debug(
"New completion for {}: {}, with pattern '{}'".format(
self._parts, model.srcmodel.__class__.__name__, pattern))
parts, model.srcmodel.__class__.__name__, pattern))
if self._model().count() == 0:
completion.hide()
@ -321,3 +302,114 @@ class Completer(QObject):
if completion.enabled:
completion.show()
def split(self, keep=False):
"""Get the text split up in parts.
Args:
keep: Whether to keep special chars and whitespace.
"""
text = self._cmd.text()[len(self._cmd.prefix()):]
if not text:
# When only ":" is entered, we already have one imaginary part,
# which just is empty at the moment.
return ['']
if not text.strip():
# Text is only whitespace so we treat this as a single element with
# the whitespace.
return [text]
runner = runners.CommandRunner(self._win_id)
parts = runner.parse(text, fallback=True, alias_no_args=False,
keep=keep)
if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, "
"inserting.".format(self._empty_item_idx))
parts.insert(self._empty_item_idx, '')
#log.completion.debug("Splitting '{}' -> {}".format(text, parts))
return parts
@pyqtSlot()
def update_cursor_part(self):
"""Get the part index of the commandline where the cursor is over."""
cursor_pos = self._cmd.cursorPosition()
snippet = slice(cursor_pos - 1, cursor_pos + 1)
if self._cmd.text()[snippet] == ' ':
spaces = True
else:
spaces = False
cursor_pos -= len(self._cmd.prefix())
parts = self.split(keep=True)
log.completion.vdebug(
"text: {}, parts: {}, cursor_pos after removing prefix '{}': "
"{}".format(self._cmd.text(), parts, self._cmd.prefix(),
cursor_pos))
for i, part in enumerate(parts):
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
if cursor_pos <= len(part):
# foo| bar
self._cursor_part = i
if spaces:
self._empty_item_idx = i
else:
self._empty_item_idx = None
log.completion.vdebug("cursor_pos {} <= len(part) {}, "
"setting cursor_part {}, empty_item_idx "
"{}".format(cursor_pos, len(part), i,
self._empty_item_idx))
break
cursor_pos -= len(part)
log.completion.vdebug(
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos))
else:
self._cursor_part = i
if spaces:
self._empty_item_idx = i
else:
self._empty_item_idx = None
log.completion.debug("cursor_part {}, spaces {}".format(
self._cursor_part, spaces))
return
def change_completed_part(self, newtext, immediate=False):
"""Change the part we're currently completing in the commandline.
Args:
text: The text to set (string).
immediate: True if the text should be completed immediately
including a trailing space and we shouldn't continue
completing the current item.
"""
parts = self.split()
log.completion.debug("changing part {} to '{}'".format(
self._cursor_part, newtext))
try:
parts[self._cursor_part] = newtext
except IndexError:
parts.append(newtext)
# We want to place the cursor directly after the part we just changed.
cursor_str = self._cmd.prefix() + ' '.join(
parts[:self._cursor_part + 1])
if immediate:
# If we should complete immediately, we want to move the cursor by
# one more char, to get to the next field.
cursor_str += ' '
text = self._cmd.prefix() + ' '.join(parts)
if immediate and self._cursor_part == len(parts) - 1:
# If we should complete immediately and we're completing the last
# part in the commandline, we automatically add a space.
text += ' '
self._cmd.setText(text)
log.completion.debug("Placing cursor after '{}'".format(cursor_str))
log.modes.debug("Completion triggered, focusing {!r}".format(self))
self._cmd.setCursorPosition(len(cursor_str))
self._cmd.setFocus()
self._cmd.show_cmd.emit()
@pyqtSlot()
def on_text_edited(self):
"""Reset _empty_item_idx if text was edited."""
self._empty_item_idx = None
# We also want to update the cursor part and emit update_completion
# here, but that's already done for us by cursorPositionChanged
# anyways, so we don't need to do it twice.

View File

@ -93,7 +93,8 @@ class CompletionView(QTreeView):
super().__init__(parent)
self._win_id = win_id
objreg.register('completion', self, scope='window', window=win_id)
completer_obj = completer.Completer(win_id, self)
cmd = objreg.get('status-command', scope='window', window=win_id)
completer_obj = completer.Completer(cmd, win_id, self)
objreg.register('completer', completer_obj, scope='window',
window=win_id)
self.enabled = config.get('completion', 'show')

View File

@ -91,11 +91,11 @@ class MainWindow(QWidget):
window=win_id)
self._vbox.addWidget(self._tabbed_browser)
self._completion = completion.CompletionView(win_id, self)
self.status = bar.StatusBar(win_id)
self._vbox.addWidget(self.status)
self._completion = completion.CompletionView(win_id, self)
self._commandrunner = runners.CommandRunner(win_id)
log.init.debug("Initializing search...")
@ -256,8 +256,6 @@ class MainWindow(QWidget):
cmd.clear_completion_selection.connect(
completion_obj.on_clear_completion_selection)
cmd.hide_completion.connect(completion_obj.hide)
cmd.update_completion.connect(completer.on_update_completion)
completer.change_completed_part.connect(cmd.on_change_completed_part)
# downloads
tabs.start_download.connect(download_manager.fetch)

View File

@ -23,7 +23,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
from PyQt5.QtWidgets import QSizePolicy
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.commands import runners, cmdexc, cmdutils
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.widgets import misc
from qutebrowser.models import cmdhistory
from qutebrowser.utils import usertypes, log, objreg
@ -34,7 +34,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"""The commandline part of the statusbar.
Attributes:
_cursor_part: The part the cursor is currently over.
_win_id: The window ID this widget is associated with.
Signals:
@ -48,10 +47,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
hidden.
hide_completion: Emitted when the completion widget should be hidden.
update_completion: Emitted when the completion should be shown/updated.
arg 0: The prefix used.
arg 1: A list of strings (commandline separated into
parts)
arg 2: The part the cursor is currently in.
show_cmd: Emitted when command input should be shown.
hide_cmd: Emitted when command input can be hidden.
"""
@ -61,7 +56,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
got_search_rev = pyqtSignal(str)
clear_completion_selection = pyqtSignal()
hide_completion = pyqtSignal()
update_completion = pyqtSignal(str, list, int)
update_completion = pyqtSignal()
show_cmd = pyqtSignal()
hide_cmd = pyqtSignal()
@ -69,13 +64,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
misc.CommandLineEdit.__init__(self, parent)
misc.MinimalLineEditMixin.__init__(self)
self._win_id = win_id
self._cursor_part = 0
self.history.history = objreg.get('command-history').data
self._empty_item_idx = None
self.textEdited.connect(self.on_text_edited)
self.cursorPositionChanged.connect(self._update_cursor_part)
self.cursorPositionChanged.connect(self.on_cursor_position_changed)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
self.cursorPositionChanged.connect(self.update_completion)
def prefix(self):
"""Get the currently entered command prefix."""
@ -87,79 +78,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
else:
return ''
def split(self, keep=False):
"""Get the text split up in parts.
Args:
keep: Whether to keep special chars and whitespace.
"""
text = self.text()[len(self.prefix()):]
if not text:
# When only ":" is entered, we already have one imaginary part,
# which just is empty at the moment.
return ['']
if not text.strip():
# Text is only whitespace so we treat this as a single element with
# the whitespace.
return [text]
runner = runners.CommandRunner(self._win_id)
parts = runner.parse(text, fallback=True, alias_no_args=False,
keep=keep)
if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, "
"inserting.".format(self._empty_item_idx))
parts.insert(self._empty_item_idx, '')
#log.completion.debug("Splitting '{}' -> {}".format(text, parts))
return parts
@pyqtSlot()
def _update_cursor_part(self):
"""Get the part index of the commandline where the cursor is over."""
cursor_pos = self.cursorPosition()
snippet = slice(cursor_pos - 1, cursor_pos + 1)
if self.text()[snippet] == ' ':
spaces = True
else:
spaces = False
cursor_pos -= len(self.prefix())
parts = self.split(keep=True)
log.completion.vdebug(
"text: {}, parts: {}, cursor_pos after removing prefix '{}': "
"{}".format(self.text(), parts, self.prefix(), cursor_pos))
for i, part in enumerate(parts):
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
if cursor_pos <= len(part):
# foo| bar
self._cursor_part = i
if spaces:
self._empty_item_idx = i
else:
self._empty_item_idx = None
log.completion.vdebug("cursor_pos {} <= len(part) {}, "
"setting cursor_part {}, empty_item_idx "
"{}".format(cursor_pos, len(part), i,
self._empty_item_idx))
break
cursor_pos -= len(part)
log.completion.vdebug(
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos))
else:
self._cursor_part = i
if spaces:
self._empty_item_idx = i
else:
self._empty_item_idx = None
log.completion.debug("cursor_part {}, spaces {}".format(
self._cursor_part, spaces))
return
@pyqtSlot()
def on_cursor_position_changed(self):
"""Update completion when the cursor position changed."""
self.update_completion.emit(self.prefix(), self.split(),
self._cursor_part)
@pyqtSlot(str)
def set_cmd_text(self, text):
"""Preset the statusbar to some text.
@ -172,8 +90,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
if old_text != text and len(old_text) == len(text):
# We want the completion to pop out here, but the cursor position
# won't change, so we make sure we emit update_completion.
self.update_completion.emit(self.prefix(), self.split(),
self._cursor_part)
self.update_completion.emit()
log.modes.debug("Setting command text, focusing {!r}".format(self))
self.setFocus()
self.show_cmd.emit()
@ -207,41 +124,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"Invalid command text '{}'.".format(text))
self.set_cmd_text(text)
@pyqtSlot(str, bool)
def on_change_completed_part(self, newtext, immediate):
"""Change the part we're currently completing in the commandline.
Args:
text: The text to set (string).
immediate: True if the text should be completed immediately
including a trailing space and we shouldn't continue
completing the current item.
"""
parts = self.split()
log.completion.debug("changing part {} to '{}'".format(
self._cursor_part, newtext))
try:
parts[self._cursor_part] = newtext
except IndexError:
parts.append(newtext)
# We want to place the cursor directly after the part we just changed.
cursor_str = self.prefix() + ' '.join(parts[:self._cursor_part + 1])
if immediate:
# If we should complete immediately, we want to move the cursor by
# one more char, to get to the next field.
cursor_str += ' '
text = self.prefix() + ' '.join(parts)
if immediate and self._cursor_part == len(parts) - 1:
# If we should complete immediately and we're completing the last
# part in the commandline, we automatically add a space.
text += ' '
self.setText(text)
log.completion.debug("Placing cursor after '{}'".format(cursor_str))
log.modes.debug("Completion triggered, focusing {!r}".format(self))
self.setCursorPosition(len(cursor_str))
self.setFocus()
self.show_cmd.emit()
@cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def command_history_prev(self):
@ -285,15 +167,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
if text[0] in signals:
signals[text[0]].emit(text.lstrip(text[0]))
@pyqtSlot(str)
def on_text_edited(self, _text):
"""Slot for textEdited. Stop history and update completion."""
self.history.stop()
self._empty_item_idx = None
# We also want to update the cursor part and emit update_completion
# here, but that's already done for us by cursorPositionChanged
# anyways, so we don't need to do it twice.
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Clear up when ommand mode was left.