Repository: qutebrowser/qutebrowser

Issue:
Option for manual completing using tab
The `completion -> show` option should be enhanced to actually not burn CPU cycles for the completion, and to show the completion when the user presses tab.

/cc @flavi0 @toofar



Discussion:
Just to make a note that I tested this for history completion (with a large history) while there isn't a delay between typing each letter anymore there is still a (for ~35000 entries on a fast PC) a delay of about 0.5s and more than that for the first completion when the model is loaded. So that is still a pretty noticeable delay after pressing tab so don't expect magic.

Yeah, I think a small delay after pressing tab isn't an issue - but having a completion and some lag when just trying to type an URL by hand might be.

So do you already happen to have a patch for this? :wink:

Oh not a good one. I just changed `completion/completionwidget.py:completion_item_next()` to enable completion and call `schedule_completion_update()`. But that breaks the down keybinding and wasn't very consistent.

See #705.


Resolving patch:
diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py
--- a/qutebrowser/completion/completer.py
+++ b/qutebrowser/completion/completer.py
@@ -19,7 +19,7 @@
 
 """Completer attached to a CompletionView."""
 
-from PyQt5.QtCore import pyqtSlot, QObject, QTimer
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
 
 from qutebrowser.config import config
 from qutebrowser.commands import cmdutils, runners
@@ -40,14 +40,22 @@ class Completer(QObject):
         _last_cursor_pos: The old cursor position so we avoid double completion
                           updates.
         _last_text: The old command text so we avoid double completion updates.
+        _signals_connected: Whether the signals are connected to update the
+                            completion when the command widget requests that.
+
+    Signals:
+        next_prev_item: Emitted to select the next/previous item in the
+                        completion.
+                        arg0: True for the previous item, False for the next.
     """
 
+    next_prev_item = pyqtSignal(bool)
+
     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._signals_connected = False
         self._ignore_change = False
         self._empty_item_idx = None
         self._timer = QTimer()
@@ -58,9 +66,63 @@ def __init__(self, cmd, win_id, parent=None):
         self._last_cursor_pos = None
         self._last_text = None
 
+        objreg.get('config').changed.connect(self.on_auto_open_changed)
+        self.handle_signal_connections()
+        self._cmd.clear_completion_selection.connect(
+            self.handle_signal_connections)
+
     def __repr__(self):
         return utils.get_repr(self)
 
+    @config.change_filter('completion', 'auto-open')
+    def on_auto_open_changed(self):
+        self.handle_signal_connections()
+
+    @pyqtSlot()
+    def handle_signal_connections(self):
+        self._connect_signals(config.get('completion', 'auto-open'))
+
+    def _connect_signals(self, connect=True):
+        """Connect or disconnect the completion signals.
+
+        Args:
+            connect: Whether to connect (True) or disconnect (False) the
+                     signals.
+
+        Return:
+            True if the signals were connected (connect=True and aren't
+            connected yet) - otherwise False.
+        """
+        connections = [
+            (self._cmd.update_completion, self.schedule_completion_update),
+            (self._cmd.textChanged, self.on_text_edited),
+        ]
+
+        if connect and not self._signals_connected:
+            for sender, receiver in connections:
+                sender.connect(receiver)
+            self._signals_connected = True
+            return True
+        elif not connect:
+            for sender, receiver in connections:
+                try:
+                    sender.disconnect(receiver)
+                except TypeError:
+                    # Don't fail if not connected
+                    pass
+            self._signals_connected = False
+        return False
+
+    def _open_completion_if_needed(self):
+        """If auto-open is false, temporarily connect signals.
+
+        Also opens the completion.
+        """
+        if not config.get('completion', 'auto-open'):
+            connected = self._connect_signals(True)
+            if connected:
+                self.update_completion()
+
     def _model(self):
         """Convienience method to get the current completion model."""
         completion = objreg.get('completion', scope='window',
@@ -405,3 +467,17 @@ def on_text_edited(self):
         # 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.
+
+    @cmdutils.register(instance='completer', hide=True,
+                       modes=[usertypes.KeyMode.command], scope='window')
+    def completion_item_prev(self):
+        """Select the previous completion item."""
+        self._open_completion_if_needed()
+        self.next_prev_item.emit(True)
+
+    @cmdutils.register(instance='completer', hide=True,
+                       modes=[usertypes.KeyMode.command], scope='window')
+    def completion_item_next(self):
+        """Select the next completion item."""
+        self._open_completion_if_needed()
+        self.next_prev_item.emit(False)
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -26,10 +26,9 @@
 from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
 
-from qutebrowser.commands import cmdutils
 from qutebrowser.config import config, style
 from qutebrowser.completion import completiondelegate, completer
-from qutebrowser.utils import usertypes, qtutils, objreg, utils
+from qutebrowser.utils import qtutils, objreg, utils
 
 
 class CompletionView(QTreeView):
@@ -96,6 +95,7 @@ def __init__(self, win_id, parent=None):
         objreg.register('completion', self, scope='window', window=win_id)
         cmd = objreg.get('status-command', scope='window', window=win_id)
         completer_obj = completer.Completer(cmd, win_id, self)
+        completer_obj.next_prev_item.connect(self.on_next_prev_item)
         objreg.register('completer', completer_obj, scope='window',
                         window=win_id)
         self.enabled = config.get('completion', 'show')
@@ -168,12 +168,15 @@ def _next_idx(self, upwards):
                 # Item is a real item, not a category header -> success
                 return idx
 
-    def _next_prev_item(self, prev):
+    @pyqtSlot(bool)
+    def on_next_prev_item(self, prev):
         """Handle a tab press for the CompletionView.
 
         Select the previous/next item and write the new text to the
         statusbar.
 
+        Called from the Completer's next_prev_item signal.
+
         Args:
             prev: True for prev item, False for next one.
         """
@@ -233,18 +236,6 @@ def on_clear_completion_selection(self):
             selmod.clearSelection()
             selmod.clearCurrentIndex()
 
-    @cmdutils.register(instance='completion', hide=True,
-                       modes=[usertypes.KeyMode.command], scope='window')
-    def completion_item_prev(self):
-        """Select the previous completion item."""
-        self._next_prev_item(prev=True)
-
-    @cmdutils.register(instance='completion', hide=True,
-                       modes=[usertypes.KeyMode.command], scope='window')
-    def completion_item_next(self):
-        """Select the next completion item."""
-        self._next_prev_item(prev=False)
-
     def selectionChanged(self, selected, deselected):
         """Extend selectionChanged to call completers selection_changed."""
         super().selectionChanged(selected, deselected)
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -352,6 +352,10 @@ def data(readonly=False):
         )),
 
         ('completion', sect.KeyValue(
+            ('auto-open',
+             SettingValue(typ.Bool(), 'true'),
+             "Automatically open completion when typing."),
+
             ('download-path-suggestion',
              SettingValue(typ.DownloadPathSuggestion(), 'path'),
              "What to display in the download filename input."),


Test patch: