From d71147898b1616488a1ee4b38b62b03d6d93515e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Nov 2014 07:49:22 +0100 Subject: [PATCH] Move completion logic from statusbar widget to completer. Fixes #247. --- qutebrowser/utils/completer.py | 180 +++++++++++++++++------ qutebrowser/widgets/completion.py | 3 +- qutebrowser/widgets/mainwindow.py | 6 +- qutebrowser/widgets/statusbar/command.py | 135 +---------------- 4 files changed, 144 insertions(+), 180 deletions(-) diff --git a/qutebrowser/utils/completer.py b/qutebrowser/utils/completer.py index 5ab750f9b..c68b2a9a2 100644 --- a/qutebrowser/utils/completer.py +++ b/qutebrowser/utils/completer.py @@ -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. diff --git a/qutebrowser/widgets/completion.py b/qutebrowser/widgets/completion.py index 78b96ef8d..670c9d2de 100644 --- a/qutebrowser/widgets/completion.py +++ b/qutebrowser/widgets/completion.py @@ -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') diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 7b9e87d85..cdbf4dd7d 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -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) diff --git a/qutebrowser/widgets/statusbar/command.py b/qutebrowser/widgets/statusbar/command.py index 9b0708fa5..34bdbff52 100644 --- a/qutebrowser/widgets/statusbar/command.py +++ b/qutebrowser/widgets/statusbar/command.py @@ -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.