Repository: qutebrowser/qutebrowser

Issue:
Escape is not handled properly in hinting mode
The following (new) test fails:

``` gherkin
Scenario: Entering, leaving and re-entering hinting mode
    When I open data/hints/html/number.html
    And I run :hint
    And I run :fake-key -g k
    And I run :fake-key -g <Esc>
    And I run :fake-key -g d
    And I wait until data/numbers/5.txt is loaded
    Then the following tabs should be open:
        - data/numbers/5.txt (active)
```

The problem is that after pressing `Esc` and re-entering the hinting mode, `k` is still remembered and the hint for `kd` is followed instead of `d`. This is very similar to #1464. Maybe also #1657 is related to the `Esc` handling.

Escape is not handled properly in hinting mode
The following (new) test fails:

``` gherkin
Scenario: Entering, leaving and re-entering hinting mode
    When I open data/hints/html/number.html
    And I run :hint
    And I run :fake-key -g k
    And I run :fake-key -g <Esc>
    And I run :fake-key -g d
    And I wait until data/numbers/5.txt is loaded
    Then the following tabs should be open:
        - data/numbers/5.txt (active)
```

The problem is that after pressing `Esc` and re-entering the hinting mode, `k` is still remembered and the hint for `kd` is followed instead of `d`. This is very similar to #1464. Maybe also #1657 is related to the `Esc` handling.



Discussion:
I don't really follow - I guess this is with `data/hints/number.html` (outside `html/`)? Also, is this in letter hint mode? I get different letters, but everything seems to work as expected for me.

Right, that should be `data/hints/number.html`. And do you have `hints -> scatter` set to false by any chance? For some reason it happens only when it's set to true (which is the default), regardless of the letters (which will be pretty hard to test consistently with scattering :sweat:).

Reproduced with `scatter` turned on - weird! Testing shouldn't be a problem, the scattering isn't random, i.e. for the same sets of link you should get the same hints.

Just noticed that this is not reproducible on every page, even with `scatter` turned on.

Next scenario which fails:

``` gherkin
Scenario: Entering and leaving numeric hinting mode
    When I open data/hints/html/simple.html
    And I run :set hints mode number
    And I run :hint
    And I run :fake-key -g <Esc>
    And I wait for "Leaving mode KeyMode.hint (reason: leave current)" in the log
    Then no crash should happen
```

Note that this is not about crash, I'm just missing `Then nothing should happen`. What happens is that instead of leaving the mode, the keyinput gets "frozen" in some undefined dead-end state.

This works fine in `letter`, but not in `number` and `word` modes.

There's a `Then ... should be logged` for those cases FWIW :wink:

Thanks, but if this was a real test, I should probably add `And I run :set hints mode letter` between `And I wait for ... in the log` and `Then nothing should happen` to not affect other scenarios. But the weirdest thing is that if I do that, the test passes already...

I hit this issue again when writing tests for the word mode in #1817. I haven't been able to track this down precisely, but know how to hopefully fix this :wink:

In the first scenario, the old key obviously stays in the keychain longer than it should. I've looked at how clearing the keychain currently works and that is by binding `Escape` to `clear-keychain ;; leave-mode` in the config. Not only this looks like users might omit `clear-keychain` without hindering usability, but considering this issue, it's somewhat flaky. If leaving the mode implies clearing the keychain in the mode to be left, it should be done directly, e.g.

``` diff
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 9aa4920..31c5406 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -282,6 +282,7 @@ class ModeManager(QObject):
             raise NotInModeError("Not in mode {}!".format(mode))
         log.modes.debug("Leaving mode {}{}".format(
             mode, '' if reason is None else ' (reason: {})'.format(reason)))
+        self.clear_keychain()
         self.mode = usertypes.KeyMode.normal
         self.left.emit(mode, self.mode, self._win_id)
```

I haven't tried eliminating `clear-keychain` from the binding yet, but I think it should work.

Since this is rather fragile, I figured that I should ask here first before making a PR, so what do you think?

I think the reason `clear-keychain` is in the keybinding is because you could press escape in normal mode to cancel a keychain without leaving anything (because you're in normal mode already).

But I don't see a reason for the keychain not to be cleared when leaving a mode, so your diff looks good.

Actually, `clear-keychain ;; leave-mode` is bound only when not in normal mode (see [here](https://github.com/The-Compiler/qutebrowser/blob/master/qutebrowser/config/configdata.py#L1454-L1456)). There is also a binding for normal mode, `clear-keychain ;; search`, which also looks redundant because `search` with empty text should clear the keychain on its own.

edit: And of course [clear-keychain](https://github.com/The-Compiler/qutebrowser/blob/master/qutebrowser/keyinput/modeman.py#L322-L325) operates only on the current mode, so clearing other modes from normal mode does not work.

Why should it? What `:search` does (clearing search highlights) seems totally independent from keychains to me.

Of course you're right, `:search` terms are not stored in keychain, so I mixed up couple of things here.

I don't really follow - I guess this is with `data/hints/number.html` (outside `html/`)? Also, is this in letter hint mode? I get different letters, but everything seems to work as expected for me.

Right, that should be `data/hints/number.html`. And do you have `hints -> scatter` set to false by any chance? For some reason it happens only when it's set to true (which is the default), regardless of the letters (which will be pretty hard to test consistently with scattering :sweat:).

Reproduced with `scatter` turned on - weird! Testing shouldn't be a problem, the scattering isn't random, i.e. for the same sets of link you should get the same hints.

Just noticed that this is not reproducible on every page, even with `scatter` turned on.

Next scenario which fails:

``` gherkin
Scenario: Entering and leaving numeric hinting mode
    When I open data/hints/html/simple.html
    And I run :set hints mode number
    And I run :hint
    And I run :fake-key -g <Esc>
    And I wait for "Leaving mode KeyMode.hint (reason: leave current)" in the log
    Then no crash should happen
```

Note that this is not about crash, I'm just missing `Then nothing should happen`. What happens is that instead of leaving the mode, the keyinput gets "frozen" in some undefined dead-end state.

This works fine in `letter`, but not in `number` and `word` modes.

There's a `Then ... should be logged` for those cases FWIW :wink:

Thanks, but if this was a real test, I should probably add `And I run :set hints mode letter` between `And I wait for ... in the log` and `Then nothing should happen` to not affect other scenarios. But the weirdest thing is that if I do that, the test passes already...

I hit this issue again when writing tests for the word mode in #1817. I haven't been able to track this down precisely, but know how to hopefully fix this :wink:

In the first scenario, the old key obviously stays in the keychain longer than it should. I've looked at how clearing the keychain currently works and that is by binding `Escape` to `clear-keychain ;; leave-mode` in the config. Not only this looks like users might omit `clear-keychain` without hindering usability, but considering this issue, it's somewhat flaky. If leaving the mode implies clearing the keychain in the mode to be left, it should be done directly, e.g.

``` diff
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 9aa4920..31c5406 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -282,6 +282,7 @@ class ModeManager(QObject):
             raise NotInModeError("Not in mode {}!".format(mode))
         log.modes.debug("Leaving mode {}{}".format(
             mode, '' if reason is None else ' (reason: {})'.format(reason)))
+        self.clear_keychain()
         self.mode = usertypes.KeyMode.normal
         self.left.emit(mode, self.mode, self._win_id)
```

I haven't tried eliminating `clear-keychain` from the binding yet, but I think it should work.

Since this is rather fragile, I figured that I should ask here first before making a PR, so what do you think?

I think the reason `clear-keychain` is in the keybinding is because you could press escape in normal mode to cancel a keychain without leaving anything (because you're in normal mode already).

But I don't see a reason for the keychain not to be cleared when leaving a mode, so your diff looks good.

Actually, `clear-keychain ;; leave-mode` is bound only when not in normal mode (see [here](https://github.com/The-Compiler/qutebrowser/blob/master/qutebrowser/config/configdata.py#L1454-L1456)). There is also a binding for normal mode, `clear-keychain ;; search`, which also looks redundant because `search` with empty text should clear the keychain on its own.

edit: And of course [clear-keychain](https://github.com/The-Compiler/qutebrowser/blob/master/qutebrowser/keyinput/modeman.py#L322-L325) operates only on the current mode, so clearing other modes from normal mode does not work.

Why should it? What `:search` does (clearing search highlights) seems totally independent from keychains to me.

Of course you're right, `:search` terms are not stored in keychain, so I mixed up couple of things here.


Resolving patch:
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -1452,7 +1452,7 @@ def data(readonly=False):
 
 KEY_DATA = collections.OrderedDict([
     ('!normal', collections.OrderedDict([
-        ('clear-keychain ;; leave-mode', ['<Escape>', '<Ctrl-[>']),
+        ('leave-mode', ['<Escape>', '<Ctrl-[>']),
     ])),
 
     ('normal', collections.OrderedDict([
@@ -1683,7 +1683,7 @@ def data(readonly=False):
     (re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'),
 
     (re.compile(r'^search *;; *clear-keychain$'), r'clear-keychain ;; search'),
-    (re.compile(r'^leave-mode$'), r'clear-keychain ;; leave-mode'),
+    (re.compile(r'^clear-keychain *;; *leave-mode$'), r'leave-mode'),
 
     (re.compile(r'^download-remove --all$'), r'download-clear'),
 
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -193,7 +193,7 @@ def _handle_single_key(self, e):
         if match == self.Match.definitive:
             self._debug_log("Definitive match for '{}'.".format(
                 self._keystring))
-            self._keystring = ''
+            self.clear_keystring()
             self.execute(binding, self.Type.chain, count)
         elif match == self.Match.ambiguous:
             self._debug_log("Ambiguous match for '{}'.".format(
@@ -205,7 +205,7 @@ def _handle_single_key(self, e):
         elif match == self.Match.none:
             self._debug_log("Giving up with '{}', no matches".format(
                 self._keystring))
-            self._keystring = ''
+            self.clear_keystring()
         else:
             raise AssertionError("Invalid match value {!r}".format(match))
         return match
@@ -271,7 +271,7 @@ def _handle_ambiguous_match(self, binding, count):
         time = config.get('input', 'timeout')
         if time == 0:
             # execute immediately
-            self._keystring = ''
+            self.clear_keystring()
             self.execute(binding, self.Type.chain, count)
         else:
             # execute in `time' ms
@@ -289,8 +289,7 @@ def delayed_exec(self, command, count):
             command/count: As if passed to self.execute()
         """
         self._debug_log("Executing delayed command now!")
-        self._keystring = ''
-        self.keystring_updated.emit(self._keystring)
+        self.clear_keystring()
         self.execute(command, self.Type.chain, count)
 
     def handle(self, e):
@@ -307,7 +306,9 @@ def handle(self, e):
         if handled or not self._supports_chains:
             return handled
         match = self._handle_single_key(e)
-        self.keystring_updated.emit(self._keystring)
+        # don't emit twice if the keystring was cleared in self.clear_keystring
+        if self._keystring:
+            self.keystring_updated.emit(self._keystring)
         return match != self.Match.none
 
     def read_config(self, modename=None):
@@ -366,6 +367,8 @@ def on_keyconfig_changed(self, mode):
 
     def clear_keystring(self):
         """Clear the currently entered key sequence."""
-        self._debug_log("discarding keystring '{}'.".format(self._keystring))
-        self._keystring = ''
-        self.keystring_updated.emit(self._keystring)
+        if self._keystring:
+            self._debug_log("discarding keystring '{}'.".format(
+                self._keystring))
+            self._keystring = ''
+            self.keystring_updated.emit(self._keystring)
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -282,6 +282,9 @@ def leave(self, mode, reason=None):
             raise NotInModeError("Not in mode {}!".format(mode))
         log.modes.debug("Leaving mode {}{}".format(
             mode, '' if reason is None else ' (reason: {})'.format(reason)))
+        # leaving a mode implies clearing keychain, see
+        # https://github.com/The-Compiler/qutebrowser/issues/1805
+        self.clear_keychain()
         self.mode = usertypes.KeyMode.normal
         self.left.emit(mode, self.mode, self._win_id)
 


Test patch:
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -278,7 +278,7 @@ def test_default_config_no_deprecated(self, rgx):
             ('search ;; clear-keychain', 'clear-keychain ;; search'),
             ('search;;clear-keychain', 'clear-keychain ;; search'),
             ('search;;foo', None),
-            ('leave-mode', 'clear-keychain ;; leave-mode'),
+            ('clear-keychain ;; leave-mode', 'leave-mode'),
             ('leave-mode ;; foo', None),
 
             ('download-remove --all', 'download-clear'),