Repository: qutebrowser/qutebrowser

Issue:
List keybindings when partial keychain entered with a count
When a partial keychain is entered, it lists all the bindings that can be pressed. However, when a count is specified this doesn't show up.

This could also be done in such a way that only lists keychains that accept a count or by showing both and indicating which ones don't accept/ignore counts.


Discussion:
cc @rcorre 

Resolving patch:
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -515,3 +515,7 @@ def validate_mode(self, mode):
             raise cmdexc.PrerequisitesError(
                 "{}: This command is only allowed in {} mode, not {}.".format(
                     self.name, mode_names, mode.name))
+
+    def takes_count(self):
+        """Return true iff this command can take a count argument."""
+        return any(arg.count for arg in self._qute_args)
diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py
--- a/qutebrowser/misc/keyhintwidget.py
+++ b/qutebrowser/misc/keyhintwidget.py
@@ -26,12 +26,14 @@
 
 import html
 import fnmatch
+import re
 
 from PyQt5.QtWidgets import QLabel, QSizePolicy
 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
 
 from qutebrowser.config import config
 from qutebrowser.utils import utils, usertypes
+from qutebrowser.commands import cmdutils
 
 
 class KeyHintView(QLabel):
@@ -85,6 +87,7 @@ def update_keyhint(self, modename, prefix):
         Args:
             prefix: The current partial keystring.
         """
+        countstr, prefix = re.match(r'^(\d*)(.*)', prefix).groups()
         if not prefix:
             self._show_timer.stop()
             self.hide()
@@ -94,11 +97,17 @@ def blacklisted(keychain):
             return any(fnmatch.fnmatchcase(keychain, glob)
                        for glob in config.val.keyhint.blacklist)
 
+        def takes_count(cmdstr):
+            cmdname = cmdstr.split(' ')[0]
+            cmd = cmdutils.cmd_dict.get(cmdname)
+            return cmd and cmd.takes_count()
+
         bindings_dict = config.key_instance.get_bindings_for(modename)
         bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
                     if k.startswith(prefix) and
                     not utils.is_special_key(k) and
-                    not blacklisted(k)]
+                    not blacklisted(k) and
+                    (takes_count(v) or not countstr)]
 
         if not bindings:
             self._show_timer.stop()


Test patch:
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -336,6 +336,7 @@ class FakeCommand:
     deprecated = attr.ib(False)
     completion = attr.ib(None)
     maxsplit = attr.ib(None)
+    takes_count = attr.ib(lambda: False)
 
 
 class FakeTimer(QObject):
diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py
--- a/tests/unit/misc/test_keyhints.py
+++ b/tests/unit/misc/test_keyhints.py
@@ -92,6 +92,23 @@ def test_suggestions(keyhint, config_stub):
         ('a', 'yellow', 'c', 'message-info cmd-ac'))
 
 
+def test_suggestions_with_count(keyhint, config_stub, monkeypatch, stubs):
+    """Test that a count prefix filters out commands that take no count."""
+    monkeypatch.setattr('qutebrowser.commands.cmdutils.cmd_dict', {
+        'foo': stubs.FakeCommand(name='foo', takes_count=lambda: False),
+        'bar': stubs.FakeCommand(name='bar', takes_count=lambda: True),
+    })
+
+    bindings = {'normal': {'aa': 'foo', 'ab': 'bar'}}
+    config_stub.val.bindings.default = bindings
+    config_stub.val.bindings.commands = bindings
+
+    keyhint.update_keyhint('normal', '2a')
+    assert keyhint.text() == expected_text(
+        ('a', 'yellow', 'b', 'bar'),
+    )
+
+
 def test_special_bindings(keyhint, config_stub):
     """Ensure a prefix of '<' doesn't suggest special keys."""
     bindings = {'normal': {