Skip to content


Remove support for ambiguous keybindings
Browse files Browse the repository at this point in the history
  • Loading branch information
The-Compiler committed Sep 13, 2017
1 parent bf9d401 commit 1fc9817
Show file tree
Hide file tree
Showing 5 changed files with 12 additions and 126 deletions.
8 changes: 0 additions & 8 deletions doc/help/settings.asciidoc
Expand Up @@ -191,7 +191,6 @@
|<<hints.uppercase,hints.uppercase>>|Make chars in hint strings uppercase.
|<<history_gap_interval,history_gap_interval>>|The maximum time in minutes between two history items for them to be considered being from the same browsing session.
|<<ignore_case,ignore_case>>|Find text on a page case-insensitively.
|<<input.ambiguous_timeout,input.ambiguous_timeout>>|Timeout (in milliseconds) for ambiguous key bindings.
|<<input.forward_unbound_keys,input.forward_unbound_keys>>|Forward unbound keys to the webview in normal mode.
|<<input.insert_mode.auto_leave,input.insert_mode.auto_leave>>|Leave insert mode if a non-editable element is clicked.
|<<input.insert_mode.auto_load,input.insert_mode.auto_load>>|Automatically enter insert mode if an editable element is focused after loading the page.
Expand Down Expand Up @@ -2050,13 +2049,6 @@ Valid values:

Default: +pass:[smart]+

== input.ambiguous_timeout
Timeout (in milliseconds) for ambiguous key bindings.
If the current input forms both a complete match and a partial match, the complete match will be executed after this time.

Default: +pass:[500]+

== input.forward_unbound_keys
Forward unbound keys to the webview in normal mode.
Expand Down
13 changes: 0 additions & 13 deletions qutebrowser/config/configdata.yml
Expand Up @@ -794,19 +794,6 @@ hints.uppercase:

## input

# FIXME:conf get rid of this?
default: 500
name: Int
minval: 0
maxval: maxint
desc: >-
Timeout (in milliseconds) for ambiguous key bindings.
If the current input forms both a complete match and a partial match, the complete
match will be executed after this time.
default: auto
Expand Down
68 changes: 5 additions & 63 deletions qutebrowser/keyinput/
Expand Up @@ -20,7 +20,6 @@
"""Base class for vim-like key sequence parser."""

import re
import functools
import unicodedata

from PyQt5.QtCore import pyqtSignal, QObject
Expand All @@ -41,7 +40,6 @@ class BaseKeyParser(QObject):
partial: No keychain matched yet, but it's still possible in the
definitive: Keychain matches exactly.
ambiguous: There are both a partial and a definitive match.
none: No more matches possible.
Types: type of a key binding.
Expand All @@ -59,7 +57,6 @@ class BaseKeyParser(QObject):
_warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them.
_keystring: The currently entered key sequence
_ambiguous_timer: Timer for delayed execution with ambiguous bindings.
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
Expand All @@ -78,16 +75,13 @@ class BaseKeyParser(QObject):
do_log = True
passthrough = False

Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
'other', 'none'])
Match = usertypes.enum('Match', ['partial', 'definitive', 'other', 'none'])
Type = usertypes.enum('Type', ['chain', 'special'])

def __init__(self, win_id, parent=None, supports_count=None,
self._win_id = win_id
self._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
self._modename = None
self._keystring = ''
if supports_count is None:
Expand Down Expand Up @@ -189,7 +183,6 @@ def _handle_single_key(self, e):
self._debug_log("Ignoring, no text char")
return self.Match.none

key_mappings = config.val.bindings.key_mappings
txt = key_mappings.get(txt, txt)
self._keystring += txt
Expand All @@ -207,10 +200,6 @@ def _handle_single_key(self, e):
self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous:
self._debug_log("Ambiguous match for '{}'.".format(
self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial:
self._debug_log("No match for '{}' (added {})".format(
self._keystring, txt))
Expand All @@ -230,11 +219,9 @@ def _match_key(self, cmd_input):
A tuple (matchtype, binding).
matchtype: Match.definitive, Match.ambiguous, Match.partial or
binding: - None with Match.partial/Match.none
- The found binding with Match.definitive/
matchtype: Match.definitive, Match.partial or Match.none.
binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive.
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
Expand All @@ -252,58 +239,13 @@ def _match_key(self, cmd_input):
elif binding.startswith(cmd_input):
partial_match = True
if definitive_match is not None and partial_match:
return (self.Match.ambiguous, definitive_match[1])
elif definitive_match is not None:
if definitive_match is not None:
return (self.Match.definitive, definitive_match[1])
elif partial_match:
return (self.Match.partial, None)
return (self.Match.none, None)

def _stop_timers(self):
"""Stop a delayed execution if any is running."""
if self._ambiguous_timer.isActive() and self.do_log:
log.keyboard.debug("Stopping delayed execution.")
except TypeError:
# no connections

def _handle_ambiguous_match(self, binding, count):
"""Handle an ambiguous match.
binding: The command-string to execute.
count: The count to pass.
self._debug_log("Ambiguous match for '{}'".format(self._keystring))
time = config.val.input.ambiguous_timeout
if time == 0:
# execute immediately
self.execute(binding, self.Type.chain, count)
# execute in `time' ms
self._debug_log("Scheduling execution of {} in {}ms".format(
binding, time))
functools.partial(self.delayed_exec, binding, count))

def delayed_exec(self, command, count):
"""Execute a delayed command.
command/count: As if passed to self.execute()
self._debug_log("Executing delayed command now!")
self.execute(command, self.Type.chain, count)

def handle(self, e):
"""Handle a new keypress and call the respective handlers.
Expand Down
10 changes: 6 additions & 4 deletions tests/end2end/features/keyinput.feature
Expand Up @@ -7,11 +7,13 @@ Feature: Keyboard input
# :clear-keychain

Scenario: Clearing the keychain
When I run :bind foo message-error test12
And I run :bind bar message-info test12-2
And I press the keys "fo"
When I run :bind ,foo message-error test12
And I run :bind ,bar message-info test12-2
And I press the keys ",fo"
And I run :clear-keychain
And I press the keys "bar"
And I press the keys ",bar"
And I run :unbind ,foo
And I run :unbind ,bar
Then the message "test12-2" should be shown

# input.forward_unbound_keys
Expand Down
39 changes: 1 addition & 38 deletions tests/unit/keyinput/
Expand Up @@ -37,7 +37,6 @@ def keyparser(key_config_stub):
0, supports_count=True, supports_chains=True)
kp.execute = mock.Mock()
yield kp
assert not kp._ambiguous_timer.isActive()

Expand Down Expand Up @@ -242,51 +241,15 @@ def test_0_press(self, handle_text, keyparser):
'message-info 0', keyparser.Type.chain, None)
assert keyparser._keystring == ''

def test_ambiguous_keychain(self, qapp, handle_text, config_stub,
config_stub.val.input.ambiguous_timeout = 100
timer = keyparser._ambiguous_timer
assert not timer.isActive()
# We start with 'a' where the keychain gives us an ambiguous result.
# Then we check if the timer has been set up correctly
handle_text((Qt.Key_A, 'a'))
assert not keyparser.execute.called
assert timer.isSingleShot()
assert timer.interval() == 100
assert timer.isActive()
# Now we type an 'x' and check 'ax' has been executed and the timer
# stopped.
handle_text((Qt.Key_X, 'x'))
'message-info ax', keyparser.Type.chain, None)
assert not timer.isActive()
assert keyparser._keystring == ''

def test_ambiguous_keychain_no_timeout(self, handle_text, config_stub,
config_stub.val.input.ambiguous_timeout = 0
def test_ambiguous_keychain(self, handle_text, keyparser):
handle_text((Qt.Key_A, 'a'))
assert keyparser.execute.called
assert not keyparser._ambiguous_timer.isActive()

def test_invalid_keychain(self, handle_text, keyparser):
handle_text((Qt.Key_B, 'b'))
handle_text((Qt.Key_C, 'c'))
assert keyparser._keystring == ''

def test_ambiguous_delayed_exec(self, handle_text, config_stub, qtbot,
config_stub.val.input.ambiguous_timeout = 100

# 'a' is an ambiguous result.
handle_text((Qt.Key_A, 'a'))
assert not keyparser.execute.called
assert keyparser._ambiguous_timer.isActive()
# We wait for the timeout to occur.
with qtbot.waitSignal(keyparser.keystring_updated):
assert keyparser.execute.called

class TestCount:

Expand Down

0 comments on commit 1fc9817

Please sign in to comment.