From 8da878c77cb217f14057cea040e8406536f2282b Mon Sep 17 00:00:00 2001
From: Florian Bruhin <git@the-compiler.org>
Date: Sun, 4 Mar 2018 17:46:26 +0100
Subject: [PATCH] Make KeySequence.matchs() work correctly

---
 qutebrowser/keyinput/keyutils.py     | 35 +++++++++++++++++++++++-----
 tests/unit/keyinput/test_keyutils.py | 30 +++++++++++++++++++++++-
 2 files changed, 58 insertions(+), 7 deletions(-)

diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index 50041b75a..ddc78b2b4 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -361,15 +361,38 @@ class KeySequence:
                 raise KeyParseError(keystr, "Got unknown key!")
 
     def matches(self, other):
-        """Check whether the given KeySequence matches with this one."""
+        """Check whether the given KeySequence matches with this one.
+
+        We store multiple QKeySequences with <= 4 keys each, so we need to match
+        those pair-wise, and account for an unequal amount of sequences as well.
+        """
         # pylint: disable=protected-access
-        assert self._sequences
-        assert other._sequences
-        for seq1, seq2 in zip(self._sequences, other._sequences):
-            match = seq1.matches(seq2)
+
+        if len(self._sequences) > len(other._sequences):
+            # If we entered more sequences than there are in the config, there's
+            # no way there can be a match.
+            return QKeySequence.NoMatch
+
+        for entered, configured in zip(self._sequences, other._sequences):
+            # If we get NoMatch/PartialMatch in a sequence, we can abort there.
+            match = entered.matches(configured)
             if match != QKeySequence.ExactMatch:
                 return match
-        return QKeySequence.ExactMatch
+
+        # We checked all common sequences and they had an ExactMatch.
+        #
+        # If there's still more sequences configured than entered, that's a
+        # PartialMatch, as more keypresses can still follow and new sequences
+        # will appear which we didn't check above.
+        #
+        # If there's the same amount of sequences configured and entered, that's
+        # an EqualMatch.
+        if len(self._sequences) == len(other._sequences):
+            return QKeySequence.ExactMatch
+        elif len(self._sequences) < len(other._sequences):
+            return QKeySequence.PartialMatch
+        else:
+            assert False, (self, other)
 
     def append_event(self, ev):
         """Create a new KeySequence object with the given QKeyEvent added.
diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py
index 0528618a8..0f13c762d 100644
--- a/tests/unit/keyinput/test_keyutils.py
+++ b/tests/unit/keyinput/test_keyutils.py
@@ -21,7 +21,7 @@ import operator
 
 import pytest
 from PyQt5.QtCore import Qt, QEvent, pyqtSignal
-from PyQt5.QtGui import QKeyEvent
+from PyQt5.QtGui import QKeyEvent, QKeySequence
 from PyQt5.QtWidgets import QWidget
 
 from tests.unit.keyinput import key_data
@@ -291,6 +291,34 @@ class TestKeySequence:
         assert s1[3:5] == s2
         assert seq[3:5] == expected
 
+    @pytest.mark.parametrize('entered, configured, expected', [
+        # config: abcd
+        ('abc', 'abcd', QKeySequence.PartialMatch),
+        ('abcd', 'abcd', QKeySequence.ExactMatch),
+        ('ax', 'abcd', QKeySequence.NoMatch),
+        ('abcdef', 'abcd', QKeySequence.NoMatch),
+
+        # config: abcd ef
+        ('abc', 'abcdef', QKeySequence.PartialMatch),
+        ('abcde', 'abcdef', QKeySequence.PartialMatch),
+        ('abcd', 'abcdef', QKeySequence.PartialMatch),
+        ('abcdx', 'abcdef', QKeySequence.NoMatch),
+        ('ax', 'abcdef', QKeySequence.NoMatch),
+        ('abcdefg', 'abcdef', QKeySequence.NoMatch),
+        ('abcdef', 'abcdef', QKeySequence.ExactMatch),
+
+        # other examples
+        ('ab', 'a', QKeySequence.NoMatch),
+
+        # empty strings
+        ('', '', QKeySequence.ExactMatch),
+        ('', 'a', QKeySequence.PartialMatch),
+        ('a', '', QKeySequence.NoMatch),
+    ])
+    def test_matches(self, entered, configured, expected):
+        entered = keyutils.KeySequence.parse(entered)
+        configured = keyutils.KeySequence.parse(configured)
+        assert entered.matches(configured) == expected
 
     @pytest.mark.parametrize('keystr, expected', [
         ('<Control-x>', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)),