From ba1bc29a976847dd856875631b98aecfc5737446 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 19 Jun 2017 16:41:17 +0200 Subject: [PATCH] Initial :bind/:unbind implementation --- qutebrowser/app.py | 2 +- qutebrowser/browser/qutescheme.py | 2 +- qutebrowser/config/config.py | 141 ++++++- qutebrowser/config/configexc.py | 7 + qutebrowser/config/configtypes.py | 12 +- qutebrowser/config/parsers/__init__.py | 20 - qutebrowser/config/parsers/keyconf.py | 441 -------------------- tests/end2end/features/completion.feature | 8 +- tests/end2end/features/hints.feature | 48 +-- tests/end2end/features/test_keyinput_bdd.py | 4 - 10 files changed, 174 insertions(+), 511 deletions(-) delete mode 100644 qutebrowser/config/parsers/__init__.py delete mode 100644 qutebrowser/config/parsers/keyconf.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index c9e0d7b1b..dd6e2c99d 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -184,7 +184,7 @@ def _process_args(args): config_obj = objreg.get('config') for opt, val in args.temp_settings: try: - config_obj.set(opt, val) + config_obj.set_str(opt, val) except configexc.Error as e: message.error("set: {} - {}".format(e.__class__.__name__, e)) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 7cf0cd0cd..c809698d9 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -457,7 +457,7 @@ def _qute_settings_set(url): return 'text/html', b'error: ' + msg.encode('utf-8') try: - config.instance.set(option, value) + config.instance.set_str(option, value) return 'text/html', b'ok' except configexc.Error as e: message.error(str(e)) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 3730f0fc6..e1824f1f3 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -27,8 +27,9 @@ import configparser from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings from qutebrowser.config import configdata, configexc, configtypes -from qutebrowser.utils import utils, objreg, message, standarddir, log -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.utils import (utils, objreg, message, standarddir, log, + usertypes) +from qutebrowser.commands import cmdexc, cmdutils, runners # An easy way to access the config from other code via config.val.foo @@ -119,10 +120,13 @@ class change_filter: # pylint: disable=invalid-name class NewKeyConfig: - def get_reverse_bindings_for(self, section): - """Get a dict of commands to a list of bindings for the section.""" + def __init__(self, manager): + self._manager = manager + + def get_reverse_bindings_for(self, mode): + """Get a dict of commands to a list of bindings for the mode.""" cmd_to_keys = {} - bindings = val.bindings.commands[section] + bindings = val.bindings.commands[mode] if bindings is None: return cmd_to_keys for key, full_cmd in bindings.items(): @@ -136,6 +140,57 @@ class NewKeyConfig: cmd_to_keys[cmd].insert(0, key) return cmd_to_keys + def _prepare(self, key, mode): + """Make sure the given mode exists and normalize the key.""" + if mode not in val.bindings.commands: + raise configexc.ValidationError( + "Invalid mode {} while binding {}!".format(mode, key)) + if utils.is_special_key(key): + # , , and should be considered equivalent + return utils.normalize_keystr(key) + return key + + def bind(self, key, command, *, mode, force=False): + """Add a new binding from key to command.""" + key = self._prepare(key, mode) + + parser = runners.CommandParser() + try: + results = parser.parse_all(command) + except cmdexc.Error as e: + # FIXME: conf good message? + raise configexc.ValidationError("Invalid command: {}".format(e)) + + for result in results: + try: + result.cmd.validate_mode(usertypes.KeyMode[mode]) + except cmdexc.PrerequisitesError as e: + # FIXME: conf good message? + raise configexc.ValidationError(str(e)) + + bindings = val.bindings.commands + + log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( + key, command, mode)) + if key in bindings[mode] and not force: + raise configexc.DuplicateKeyError("Duplicate key {}".format(key)) + bindings[mode][key] = command + val.bindings.commands = bindings # FIXME:conf + + def unbind(self, key, *, mode='normal'): + """Unbind the given key in the given mode.""" + key = self._prepare(key, mode) + try: + del val.bindings.commands[mode][key] + except KeyError: + raise configexc.ValidationError("Unknown binding {}".format(key)) + val.bindings.commands = val.bindings.commands # FIXME:conf + + def get_command(self, key, mode): + """Get the command for a given key (or None).""" + key = self._prepare(key, mode) + return val.bindings.commands[mode].get(key, None) + class ConfigCommands: @@ -200,7 +255,7 @@ class ConfigCommands: if len(values) == 1: # If we have only one value, just set it directly (avoid # breaking stuff like aliases or other pseudo-settings) - self._config.set(option, values[0]) + self._config.set_str(option, values[0]) return # Use the next valid value from values, or the first if the current @@ -212,7 +267,7 @@ class ConfigCommands: value = values[idx] except ValueError: value = values[0] - self._config.set(option, value) + self._config.set_str(option, value) @contextlib.contextmanager def _handle_config_error(self): @@ -225,6 +280,56 @@ class ConfigCommands: raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e)) + @cmdutils.register(instance='config-commands', maxsplit=1, + no_cmd_split=True, no_replace_variables=True) + @cmdutils.argument('command', completion=usertypes.Completion.bind) + def bind(self, key, command=None, *, mode='normal', force=False): + """Bind a key to a command. + + Args: + key: The keychain or special key (inside `<...>`) to bind. + command: The command to execute, with optional args, or None to + print the current binding. + mode: A comma-separated list of modes to bind the key in + (default: `normal`). + force: Rebind the key if it is already bound. + """ + if utils.is_special_key(key): + # , , and should be considered equivalent + key = utils.normalize_keystr(key) + + if mode not in val.bindings.commands: + raise cmdexc.CommandError("Invalid mode {}!".format(mode)) + + if command is None: + cmd = key_instance.get_command(key, mode) + if cmd is None: + message.info("{} is unbound in {} mode".format(key, mode)) + else: + message.info("{} is bound to '{}' in {} mode".format( + key, cmd, mode)) + return + + try: + key_instance.bind(key, command, mode=mode, force=force) + except configexc.DuplicateKeyError as e: + raise cmdexc.CommandError(str(e) + " - use --force to override!") + except configexc.ValidationError as e: + raise cmdexc.CommandError(str(e)) + + @cmdutils.register(instance='config-commands') + def unbind(self, key, mode='normal'): + """Unbind a keychain. + + Args: + key: The keychain or special key (inside <...>) to unbind. + mode: A mode to unbind the key in (default: `normal`). + """ + try: + key_instance.unbind(key, mode=mode) + except configexc.ValidationError as e: + raise cmdexc.CommandError(str(e)) + class NewConfigManager(QObject): @@ -235,6 +340,10 @@ class NewConfigManager(QObject): self.options = {} self._values = {} # FIXME:conf stub + def _changed(self, name, value): + self.changed.emit(name) + log.config.debug("Config option changed: {} = {}".format(name, value)) + def read_defaults(self): for name, option in configdata.DATA.items(): self.options[name] = option @@ -256,11 +365,17 @@ class NewConfigManager(QObject): return opt.typ.to_str(value) def set(self, name, value): + # FIXME:conf stub + opt = self.get_opt(name) + opt.typ.to_py(value) # for validation + self._values[name] = value + self._changed(name, value) + + def set_str(self, name, value): # FIXME:conf stub opt = self.get_opt(name) self._values[name] = opt.typ.from_str(value) - self.changed.emit(name) - log.config.debug("Config option changed: {} = {}".format(name, value)) + self._changed(name, value) def dump_userconfig(self): """Get the part of the config which was changed by the user. @@ -301,9 +416,13 @@ class ConfigContainer: Those two never overlap as configdata.py ensures there are no shadowing options. """ + if attr.startswith('_'): + return self.__getattribute__(attr) + name = self._join(attr) if configdata.is_valid_prefix(name): return ConfigContainer(manager=self._manager, prefix=name) + try: return self._manager.get(name) except configexc.NoOptionError as e: @@ -313,7 +432,7 @@ class ConfigContainer: def __setattr__(self, attr, value): if attr.startswith('_'): return super().__setattr__(attr, value) - self._handler(self._join(attr), value) + self._manager.set(self._join(attr), value) def _join(self, attr): if self._prefix: @@ -364,7 +483,7 @@ def init(parent=None): global val, instance, key_instance val = ConfigContainer(new_config) instance = new_config - key_instance = NewKeyConfig() + key_instance = NewKeyConfig(new_config) for cf in _change_filters: cf.validate() diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index c13d94d25..99e2a1a15 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -52,6 +52,13 @@ class ValidationError(Error): self.option = None +class DuplicateKeyError(ValidationError): + + """Raised when there was a duplicate key.""" + + pass + + class NoOptionError(Error): """Raised when an option was not found.""" diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index b4918cd47..e940041ed 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -729,9 +729,15 @@ class Command(BaseType): self._basic_py_validation(value, str) if not value: return - split = value.split() - if not split or split[0] not in cmdutils.cmd_dict: - raise configexc.ValidationError(value, "must be a valid command!") + + # FIXME:conf is it okay to import runners.py here? + from qutebrowser.commands import runners, cmdexc + parser = runners.CommandParser() + try: + parser.parse_all(value) + except cmdexc.Error as e: + raise configexc.ValidationError(value, str(e)) + return value def complete(self): diff --git a/qutebrowser/config/parsers/__init__.py b/qutebrowser/config/parsers/__init__.py deleted file mode 100644 index 1c316078d..000000000 --- a/qutebrowser/config/parsers/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Parser for different configuration formats.""" diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py deleted file mode 100644 index a51e5ae55..000000000 --- a/qutebrowser/config/parsers/keyconf.py +++ /dev/null @@ -1,441 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Parser for the key configuration.""" - -import collections -import os.path -import itertools - -from PyQt5.QtCore import pyqtSignal, QObject - -from qutebrowser.config import configdata, textwrapper -from qutebrowser.commands import cmdutils, cmdexc -from qutebrowser.utils import log, utils, qtutils, message, usertypes - - -class KeyConfigError(Exception): - - """Raised on errors with the key config. - - Attributes: - lineno: The config line in which the exception occurred. - """ - - def __init__(self, msg=None): - super().__init__(msg) - self.lineno = None - - -class DuplicateKeychainError(KeyConfigError): - - """Error raised when there's a duplicate key binding.""" - - def __init__(self, keychain): - super().__init__("Duplicate key chain {}!".format(keychain)) - self.keychain = keychain - - -class KeyConfigParser(QObject): - - """Parser for the keybind config. - - Attributes: - _configfile: The filename of the config or None. - _cur_section: The section currently being processed by _read(). - _cur_command: The command currently being processed by _read(). - is_dirty: Whether the config is currently dirty. - - Class attributes: - UNBOUND_COMMAND: The special command used for unbound keybindings. - - Signals: - changed: Emitted when the internal data has changed. - arg: Name of the mode which was changed. - config_dirty: Emitted when the config should be re-saved. - """ - - changed = pyqtSignal(str) - config_dirty = pyqtSignal() - UNBOUND_COMMAND = '' - - def __init__(self, configdir, fname, relaxed=False, parent=None): - """Constructor. - - Args: - configdir: The directory to save the configs in. - fname: The filename of the config. - relaxed: If given, unknown commands are ignored. - """ - super().__init__(parent) - self.is_dirty = False - self._cur_section = None - self._cur_command = None - # Mapping of section name(s) to key binding -> command dicts. - self.keybindings = collections.OrderedDict() - self._configfile = os.path.join(configdir, fname) - - if not os.path.exists(self._configfile): - self._load_default() - else: - self._read(relaxed) - self._load_default(only_new=True) - log.init.debug("Loaded bindings: {}".format(self.keybindings)) - - def __str__(self): - """Get the config as string.""" - lines = configdata.KEY_FIRST_COMMENT.strip('\n').splitlines() - lines.append('') - for sectname, sect in self.keybindings.items(): - lines.append('[{}]'.format(sectname)) - lines += self._str_section_desc(sectname) - lines.append('') - data = collections.OrderedDict() - for key, cmd in sect.items(): - if cmd in data: - data[cmd].append(key) - else: - data[cmd] = [key] - for cmd, keys in data.items(): - lines.append(cmd) - for k in keys: - lines.append(' ' * 4 + k) - lines.append('') - return '\n'.join(lines) + '\n' - - def __repr__(self): - return utils.get_repr(self, constructor=True, - configfile=self._configfile) - - def _str_section_desc(self, sectname): - """Get the section description string for sectname.""" - wrapper = textwrapper.TextWrapper() - lines = [] - try: - seclines = configdata.KEY_SECTION_DESC[sectname].splitlines() - except KeyError: - return [] - else: - for secline in seclines: - if 'http://' in secline or 'https://' in secline: - lines.append('# ' + secline) - else: - lines += wrapper.wrap(secline) - return lines - - def save(self): - """Save the key config file.""" - log.destroy.debug("Saving key config to {}".format(self._configfile)) - - try: - with qtutils.savefile_open(self._configfile, - encoding='utf-8') as f: - data = str(self) - f.write(data) - except OSError as e: - message.error("Could not save key config: {}".format(e)) - - @cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True, - no_replace_variables=True) - @cmdutils.argument('command', completion=usertypes.Completion.bind) - def bind(self, key, command=None, *, mode='normal', force=False): - """Bind a key to a command. - - Args: - key: The keychain or special key (inside `<...>`) to bind. - command: The command to execute, with optional args, or None to - print the current binding. - mode: A comma-separated list of modes to bind the key in - (default: `normal`). - force: Rebind the key if it is already bound. - """ - if utils.is_special_key(key): - # , , and should be considered equivalent - key = key.lower() - - if command is None: - cmd = self.get_bindings_for(mode).get(key, None) - if cmd is None: - message.info("{} is unbound in {} mode".format(key, mode)) - else: - message.info("{} is bound to '{}' in {} mode".format(key, cmd, - mode)) - return - - modenames = self._normalize_sectname(mode).split(',') - for m in modenames: - if m not in configdata.KEY_DATA: - raise cmdexc.CommandError("Invalid mode {}!".format(m)) - try: - modes = [usertypes.KeyMode[m] for m in modenames] - self._validate_command(command, modes) - except KeyConfigError as e: - raise cmdexc.CommandError(str(e)) - try: - self._add_binding(mode, key, command, force=force) - except DuplicateKeychainError as e: - raise cmdexc.CommandError("Duplicate keychain {} - use --force to " - "override!".format(str(e.keychain))) - except KeyConfigError as e: - raise cmdexc.CommandError(e) - for m in modenames: - self.changed.emit(m) - self._mark_config_dirty() - - @cmdutils.register(instance='key-config') - def unbind(self, key, mode='normal'): - """Unbind a keychain. - - Args: - key: The keychain or special key (inside <...>) to unbind. - mode: A comma-separated list of modes to unbind the key in - (default: `normal`). - """ - if utils.is_special_key(key): - # , , and should be considered equivalent - key = key.lower() - - mode = self._normalize_sectname(mode) - for m in mode.split(','): - if m not in configdata.KEY_DATA: - raise cmdexc.CommandError("Invalid mode {}!".format(m)) - try: - sect = self.keybindings[mode] - except KeyError: - raise cmdexc.CommandError("Can't find mode section '{}'!".format( - mode)) - try: - del sect[key] - except KeyError: - raise cmdexc.CommandError("Can't find binding '{}' in section " - "'{}'!".format(key, mode)) - else: - if key in itertools.chain.from_iterable( - configdata.KEY_DATA[mode].values()): - try: - self._add_binding(mode, key, self.UNBOUND_COMMAND) - except DuplicateKeychainError: - pass - for m in mode.split(','): - self.changed.emit(m) - self._mark_config_dirty() - - def _normalize_sectname(self, s): - """Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'.""" - if s.startswith('!'): - inverted = True - s = s[1:] - else: - inverted = False - sections = ','.join(sorted(s.split(','))) - if inverted: - sections = '!' + sections - return sections - - def _load_default(self, *, only_new=False): - """Load the built-in default key bindings. - - Args: - only_new: If set, only keybindings which are completely unused - (same command/key not bound) are added. - """ - # {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...} - bindings_to_add = collections.OrderedDict() - mark_dirty = False - - for sectname, sect in configdata.KEY_DATA.items(): - sectname = self._normalize_sectname(sectname) - bindings_to_add[sectname] = collections.OrderedDict() - for command, keychains in sect.items(): - for e in keychains: - if not only_new or self._is_new(sectname, command, e): - assert e not in bindings_to_add[sectname] - bindings_to_add[sectname][e] = command - mark_dirty = True - - for sectname, sect in bindings_to_add.items(): - if not sect: - if not only_new: - self.keybindings[sectname] = collections.OrderedDict() - else: - for keychain, command in sect.items(): - self._add_binding(sectname, keychain, command) - self.changed.emit(sectname) - - if mark_dirty: - self._mark_config_dirty() - - def _is_new(self, sectname, command, keychain): - """Check if a given binding is new. - - A binding is considered new if both the command is not bound to any key - yet, and the key isn't used anywhere else in the same section. - """ - if utils.is_special_key(keychain): - keychain = keychain.lower() - - try: - bindings = self.keybindings[sectname] - except KeyError: - return True - if keychain in bindings: - return False - else: - return command not in bindings.values() - - def _read(self, relaxed=False): - """Read the config file from disk and parse it. - - Args: - relaxed: Ignore unknown commands. - """ - try: - with open(self._configfile, 'r', encoding='utf-8') as f: - for i, line in enumerate(f): - line = line.rstrip() - try: - if not line.strip() or line.startswith('#'): - continue - elif line.startswith('[') and line.endswith(']'): - sectname = line[1:-1] - self._cur_section = self._normalize_sectname( - sectname) - elif line.startswith((' ', '\t')): - line = line.strip() - self._read_keybinding(line) - else: - line = line.strip() - self._read_command(line) - except (KeyConfigError, cmdexc.CommandError) as e: - if relaxed: - continue - else: - e.lineno = i - raise - except OSError: - log.keyboard.exception("Failed to read key bindings!") - for sectname in self.keybindings: - self.changed.emit(sectname) - - def _mark_config_dirty(self): - """Mark the config as dirty.""" - self.is_dirty = True - self.config_dirty.emit() - - def _validate_command(self, line, modes=None): - """Check if a given command is valid. - - Args: - line: The commandline to validate. - modes: A list of modes to validate the commands for, or None. - """ - from qutebrowser.config import config - if line == self.UNBOUND_COMMAND: - return - commands = line.split(';;') - try: - first_cmd = commands[0].split(maxsplit=1)[0].strip() - cmd = cmdutils.cmd_dict[first_cmd] - if cmd.no_cmd_split: - commands = [line] - except (KeyError, IndexError): - pass - - for cmd in commands: - if not cmd.strip(): - raise KeyConfigError("Got empty command (line: {!r})!".format( - line)) - commands = [c.split(maxsplit=1)[0].strip() for c in commands] - for cmd in commands: - # FIXME:conf - # aliases = config.section('aliases') - if cmd in cmdutils.cmd_dict: - cmdname = cmd - # elif cmd in aliases: - # cmdname = aliases[cmd].split(maxsplit=1)[0].strip() - else: - raise KeyConfigError("Invalid command '{}'!".format(cmd)) - cmd_obj = cmdutils.cmd_dict[cmdname] - for m in modes or []: - cmd_obj.validate_mode(m) - - def _read_command(self, line): - """Read a command from a line.""" - if self._cur_section is None: - raise KeyConfigError("Got command '{}' without getting a " - "section!".format(line)) - else: - for rgx, repl in configdata.CHANGED_KEY_COMMANDS: - if rgx.match(line): - line = rgx.sub(repl, line) - self._mark_config_dirty() - break - self._validate_command(line) - self._cur_command = line - - def _read_keybinding(self, line): - """Read a key binding from a line.""" - if self._cur_command is None: - raise KeyConfigError("Got key binding '{}' without getting a " - "command!".format(line)) - else: - assert self._cur_section is not None - self._add_binding(self._cur_section, line, self._cur_command) - - def _add_binding(self, sectname, keychain, command, *, force=False): - """Add a new binding from keychain to command in section sectname.""" - if utils.is_special_key(keychain): - # , , and should be considered equivalent - keychain = keychain.lower() - log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( - keychain, command, sectname)) - if sectname not in self.keybindings: - self.keybindings[sectname] = collections.OrderedDict() - if keychain in self.get_bindings_for(sectname): - if force or command == self.UNBOUND_COMMAND: - self.unbind(keychain, mode=sectname) - else: - raise DuplicateKeychainError(keychain) - section = self.keybindings[sectname] - if (command != self.UNBOUND_COMMAND and - section.get(keychain, None) == self.UNBOUND_COMMAND): - # re-binding an unbound keybinding - del section[keychain] - self.keybindings[sectname][keychain] = command - - def get_bindings_for(self, section): - """Get a dict with all merged key bindings for a section.""" - bindings = {} - for sectstring, d in self.keybindings.items(): - if sectstring.startswith('!'): - inverted = True - sectstring = sectstring[1:] - else: - inverted = False - sects = [s.strip() for s in sectstring.split(',')] - matches = any(s == section for s in sects) - if (not inverted and matches) or (inverted and not matches): - bindings.update(d) - try: - bindings.update(self.keybindings['all']) - except KeyError: - pass - bindings = {k: v for k, v in bindings.items() - if v != self.UNBOUND_COMMAND} - return bindings diff --git a/tests/end2end/features/completion.feature b/tests/end2end/features/completion.feature index 84dd50ce3..94c194e4f 100644 --- a/tests/end2end/features/completion.feature +++ b/tests/end2end/features/completion.feature @@ -46,11 +46,9 @@ Feature: Using completion When I run :set-cmd-text -s :bookmark-load Then the completion model should be BookmarkCompletionModel - # FIXME:conf - - # Scenario: Using bind completion - # When I run :set-cmd-text -s :bind X - # Then the completion model should be BindCompletionModel + Scenario: Using bind completion + When I run :set-cmd-text -s :bind X + Then the completion model should be BindCompletionModel Scenario: Using session completion Given I open data/hello.txt diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index 872867625..3f7e8a002 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -243,32 +243,30 @@ Feature: Using hints ### hints.auto_follow-timeout - ## FIXME:conf + @not_osx + Scenario: Ignoring key presses after auto-following hints + When I set hints.auto_follow_timeout to 1000 + And I set hints.mode to number + And I run :bind --force , message-error "This error message was triggered via a keybinding which should have been inhibited" + And I open data/hints/html/simple.html + And I hint with args "all" + And I press the key "f" + And I wait until data/hello.txt is loaded + And I press the key "," + # Waiting here so we don't affect the next test + And I wait for "Releasing inhibition state of normal mode." in the log + Then "Ignoring key ',', because the normal mode is currently inhibited." should be logged - # @not_osx - # Scenario: Ignoring key presses after auto-following hints - # When I set hints.auto_follow_timeout to 1000 - # And I set hints.mode to number - # And I run :bind --force , message-error "This error message was triggered via a keybinding which should have been inhibited" - # And I open data/hints/html/simple.html - # And I hint with args "all" - # And I press the key "f" - # And I wait until data/hello.txt is loaded - # And I press the key "," - # # Waiting here so we don't affect the next test - # And I wait for "Releasing inhibition state of normal mode." in the log - # Then "Ignoring key ',', because the normal mode is currently inhibited." should be logged - - # Scenario: Turning off auto-follow-timeout - # When I set hints.auto_follow_timeout to 0 - # And I set hints.mode to number - # And I run :bind --force , message-info "Keypress worked!" - # And I open data/hints/html/simple.html - # And I hint with args "all" - # And I press the key "f" - # And I wait until data/hello.txt is loaded - # And I press the key "," - # Then the message "Keypress worked!" should be shown + Scenario: Turning off auto-follow-timeout + When I set hints.auto_follow_timeout to 0 + And I set hints.mode to number + And I run :bind --force , message-info "Keypress worked!" + And I open data/hints/html/simple.html + And I hint with args "all" + And I press the key "f" + And I wait until data/hello.txt is loaded + And I press the key "," + Then the message "Keypress worked!" should be shown ### Word hints diff --git a/tests/end2end/features/test_keyinput_bdd.py b/tests/end2end/features/test_keyinput_bdd.py index 8eb449b10..461db491c 100644 --- a/tests/end2end/features/test_keyinput_bdd.py +++ b/tests/end2end/features/test_keyinput_bdd.py @@ -21,7 +21,3 @@ import pytest import pytest_bdd as bdd bdd.scenarios('keyinput.feature') - -## FIXME:conf -pytestmark = pytest.mark.skipif(True, reason="FIXME:conf") -