diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2057265a8..9652f9b41 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -59,6 +59,7 @@ Changed the most recently focused (instead of the last opened) window. This can be configured with the new `new-instance-open-target.window` setting. - Word hints now are more clever about getting the element text from some elements. +- Completions for `:help` and `:bind` now also show hidden commands Removed ~~~~~~~ diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py index d3b49c0ec..6359d3771 100644 --- a/qutebrowser/completion/models/instances.py +++ b/qutebrowser/completion/models/instances.py @@ -114,6 +114,13 @@ def init_session_completion(): _instances[usertypes.Completion.sessions] = model +def _init_bind_completion(): + """Initialize the command completion model.""" + log.completion.debug("Initializing bind completion.") + model = miscmodels.BindCompletionModel() + _instances[usertypes.Completion.bind] = model + + INITIALIZERS = { usertypes.Completion.command: _init_command_completion, usertypes.Completion.helptopic: _init_helptopic_completion, @@ -125,6 +132,7 @@ INITIALIZERS = { usertypes.Completion.quickmark_by_name: init_quickmark_completions, usertypes.Completion.bookmark_by_url: init_bookmark_completions, usertypes.Completion.sessions: init_session_completion, + usertypes.Completion.bind: _init_bind_completion, } @@ -182,5 +190,7 @@ def init(): keyconf = objreg.get('key-config') keyconf.changed.connect( functools.partial(update, [usertypes.Completion.command])) + keyconf.changed.connect( + functools.partial(update, [usertypes.Completion.bind])) objreg.get('config').changed.connect(_update_aliases) diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index bbbaa8a9d..aeb9478f0 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -30,7 +30,7 @@ from qutebrowser.completion.models import base class CommandCompletionModel(base.BaseCompletionModel): - """A CompletionModel filled with all commands and descriptions.""" + """A CompletionModel filled with non-hidden commands and descriptions.""" # https://github.com/The-Compiler/qutebrowser/issues/545 # pylint: disable=abstract-method @@ -39,23 +39,11 @@ class CommandCompletionModel(base.BaseCompletionModel): def __init__(self, parent=None): super().__init__(parent) - assert cmdutils.cmd_dict - cmdlist = [] - for obj in set(cmdutils.cmd_dict.values()): - if (obj.hide or (obj.debug and not objreg.get('args').debug) or - obj.deprecated): - pass - else: - cmdlist.append((obj.name, obj.desc)) - for name, cmd in config.section('aliases').items(): - cmdlist.append((name, "Alias for '{}'".format(cmd))) + cmdlist = _get_cmd_completions(include_aliases=True, + include_hidden=False) cat = self.new_category("Commands") - - # map each command to its bound keys and show these in the misc column - key_config = objreg.get('key-config') - cmd_to_keys = key_config.get_reverse_bindings_for('normal') - for (name, desc) in sorted(cmdlist): - self.new_item(cat, name, desc, ', '.join(cmd_to_keys[name])) + for (name, desc, misc) in cmdlist: + self.new_item(cat, name, desc, misc) class HelpCompletionModel(base.BaseCompletionModel): @@ -72,17 +60,11 @@ class HelpCompletionModel(base.BaseCompletionModel): def _init_commands(self): """Fill completion with :command entries.""" - assert cmdutils.cmd_dict - cmdlist = [] - for obj in set(cmdutils.cmd_dict.values()): - if (obj.hide or (obj.debug and not objreg.get('args').debug) or - obj.deprecated): - pass - else: - cmdlist.append((':' + obj.name, obj.desc)) + cmdlist = _get_cmd_completions(include_aliases=False, + include_hidden=True, prefix=':') cat = self.new_category("Commands") - for (name, desc) in sorted(cmdlist): - self.new_item(cat, name, desc) + for (name, desc, misc) in cmdlist: + self.new_item(cat, name, desc, misc) def _init_settings(self): """Fill completion with section->option entries.""" @@ -259,3 +241,49 @@ class TabCompletionModel(base.BaseCompletionModel): tabbed_browser = objreg.get('tabbed-browser', scope='window', window=int(win_id)) tabbed_browser.on_tab_close_requested(int(tab_index) - 1) + + +class BindCompletionModel(base.BaseCompletionModel): + + """A CompletionModel filled with all bindable commands and descriptions.""" + + # https://github.com/The-Compiler/qutebrowser/issues/545 + # pylint: disable=abstract-method + + COLUMN_WIDTHS = (20, 60, 20) + + def __init__(self, parent=None): + super().__init__(parent) + cmdlist = _get_cmd_completions(include_hidden=True, + include_aliases=True) + cat = self.new_category("Commands") + for (name, desc, misc) in cmdlist: + self.new_item(cat, name, desc, misc) + + +def _get_cmd_completions(include_hidden, include_aliases, prefix=''): + """Get a list of completions info for commands, sorted by name. + + Args: + include_hidden: True to include commands annotated with hide=True. + include_aliases: True to include command aliases. + prefix: String to append to the command name. + + Return: A list of tuples of form (name, description, bindings). + """ + assert cmdutils.cmd_dict + cmdlist = [] + cmd_to_keys = objreg.get('key-config').get_reverse_bindings_for('normal') + for obj in set(cmdutils.cmd_dict.values()): + hide_debug = obj.debug and not objreg.get('args').debug + hide_hidden = obj.hide and not include_hidden + if not (hide_debug or hide_hidden or obj.deprecated): + bindings = ', '.join(cmd_to_keys.get(obj.name, [])) + cmdlist.append((prefix + obj.name, obj.desc, bindings)) + + if include_aliases: + for name, cmd in config.section('aliases').items(): + bindings = ', '.join(cmd_to_keys.get(name, [])) + cmdlist.append((name, "Alias for '{}'".format(cmd), bindings)) + + return cmdlist diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 2eebd6bd9..c994913db 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -153,7 +153,7 @@ class KeyConfigParser(QObject): @cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('win_id', win_id=True) - @cmdutils.argument('command', completion=usertypes.Completion.command) + @cmdutils.argument('command', completion=usertypes.Completion.bind) def bind(self, key, win_id, command=None, *, mode='normal', force=False): """Bind a key to a command. @@ -424,8 +424,9 @@ class KeyConfigParser(QObject): def get_reverse_bindings_for(self, section): """Get a dict of commands to a list of bindings for the section.""" - cmd_to_keys = collections.defaultdict(list) + cmd_to_keys = {} for key, cmd in self.get_bindings_for(section).items(): + cmd_to_keys.setdefault(cmd, []) # put special bindings last if utils.is_special_key(key): cmd_to_keys[cmd].append(key) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 1cd1f0385..ea7662e41 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -238,7 +238,8 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', # Available command completions Completion = enum('Completion', ['command', 'section', 'option', 'value', 'helptopic', 'quickmark_by_name', - 'bookmark_by_url', 'url', 'tab', 'sessions']) + 'bookmark_by_url', 'url', 'tab', 'sessions', + 'bind']) # Exit statuses for errors. Needs to be an int for sys.exit. diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 6d4f66c34..83f8060e1 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -31,17 +31,18 @@ from qutebrowser.browser import history from qutebrowser.config import sections, value -def _get_completions(model): - """Collect all the completion entries of a model, organized by category. +def _check_completions(model, expected): + """Check that a model contains the expected items in any order. - The result is a list of form: - [ - (CategoryName: [(name, desc, misc), ...]), - (CategoryName: [(name, desc, misc), ...]), - ... - ] + Args: + expected: A dict of form + { + CategoryName: [(name, desc, misc), ...], + CategoryName: [(name, desc, misc), ...], + ... + } """ - completions = [] + actual = {} for i in range(0, model.rowCount()): category = model.item(i) entries = [] @@ -50,8 +51,12 @@ def _get_completions(model): desc = category.child(j, 1) misc = category.child(j, 2) entries.append((name.text(), desc.text(), misc.text())) - completions.append((category.text(), entries)) - return completions + actual[category.text()] = entries + for cat_name, expected_entries in expected.items(): + assert cat_name in actual + actual_items = actual[cat_name] + for expected_item in expected_entries: + assert expected_item in actual_items def _patch_cmdutils(monkeypatch, stubs, symbol): @@ -165,7 +170,6 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub, Validates that: - only non-hidden and non-deprecated commands are included - - commands are sorted by name - the command description is shown in the desc column - the binding (if any) is shown in the misc column - aliases are included @@ -173,55 +177,56 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub, _patch_cmdutils(monkeypatch, stubs, 'qutebrowser.completion.models.miscmodels.cmdutils') config_stub.data['aliases'] = {'rock': 'roll'} - key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'}) + key_config_stub.set_bindings_for('normal', {'s': 'stop', + 'rr': 'roll', + 'ro': 'rock'}) model = miscmodels.CommandCompletionModel() qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Commands", [ + _check_completions(model, { + "Commands": [ + ('stop', 'stop qutebrowser', 's'), ('drop', 'drop all user data', ''), - ('rock', "Alias for 'roll'", ''), ('roll', 'never gonna give you up', 'rr'), - ('stop', 'stop qutebrowser', 's') - ]) - ] + ('rock', "Alias for 'roll'", 'ro'), + ] + }) -def test_help_completion(qtmodeltester, monkeypatch, stubs): +def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub): """Test the results of command completion. Validates that: - - only non-hidden and non-deprecated commands are included - - commands are sorted by name + - only non-deprecated commands are included - the command description is shown in the desc column - the binding (if any) is shown in the misc column - aliases are included - only the first line of a multiline description is shown """ module = 'qutebrowser.completion.models.miscmodels' + key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'}) _patch_cmdutils(monkeypatch, stubs, module + '.cmdutils') _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') model = miscmodels.HelpCompletionModel() qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Commands", [ + _check_completions(model, { + "Commands": [ + (':stop', 'stop qutebrowser', 's'), (':drop', 'drop all user data', ''), - (':roll', 'never gonna give you up', ''), - (':stop', 'stop qutebrowser', '') - ]), - ("Settings", [ + (':roll', 'never gonna give you up', 'rr'), + (':hide', '', ''), + ], + "Settings": [ ('general->time', 'Is an illusion.', ''), ('general->volume', 'Goes to 11', ''), ('ui->gesture', 'Waggle your hands to control qutebrowser', ''), ('ui->mind', 'Enable mind-control ui (experimental)', ''), ('ui->voice', 'Whether to respond to voice commands', ''), - ]) - ] + ] + }) def test_quickmark_completion(qtmodeltester, quickmarks): @@ -230,14 +235,13 @@ def test_quickmark_completion(qtmodeltester, quickmarks): qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Quickmarks", [ + _check_completions(model, { + "Quickmarks": [ ('aw', 'https://wiki.archlinux.org', ''), ('ddg', 'https://duckduckgo.com', ''), ('wiki', 'https://wikipedia.org', ''), - ]) - ] + ] + }) def test_bookmark_completion(qtmodeltester, bookmarks): @@ -246,14 +250,13 @@ def test_bookmark_completion(qtmodeltester, bookmarks): qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Bookmarks", [ + _check_completions(model, { + "Bookmarks": [ ('https://github.com', 'GitHub', ''), ('https://python.org', 'Welcome to Python.org', ''), ('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''), - ]) - ] + ] + }) def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks, @@ -271,23 +274,22 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks, qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Quickmarks", [ + _check_completions(model, { + "Quickmarks": [ ('https://wiki.archlinux.org', 'aw', ''), ('https://duckduckgo.com', 'ddg', ''), ('https://wikipedia.org', 'wiki', ''), - ]), - ("Bookmarks", [ + ], + "Bookmarks": [ ('https://github.com', 'GitHub', ''), ('https://python.org', 'Welcome to Python.org', ''), ('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''), - ]), - ("History", [ + ], + "History": [ ('https://python.org', 'Welcome to Python.org', '2016-03-08'), ('https://github.com', 'GitHub', '2016-05-01'), - ]), - ] + ], + }) def test_url_completion_delete_bookmark(qtmodeltester, config_stub, @@ -332,10 +334,9 @@ def test_session_completion(qtmodeltester, session_manager_stub): qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Sessions", [('default', '', ''), ('1', '', ''), ('2', '', '')]) - ] + _check_completions(model, { + "Sessions": [('default', '', ''), ('1', '', ''), ('2', '', '')] + }) def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry, @@ -352,17 +353,16 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry, qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ('0', [ + _check_completions(model, { + '0': [ ('0/1', 'https://github.com', 'GitHub'), ('0/2', 'https://wikipedia.org', 'Wikipedia'), ('0/3', 'https://duckduckgo.com', 'DuckDuckGo') - ]), - ('1', [ + ], + '1': [ ('1/1', 'https://wiki.archlinux.org', 'ArchWiki'), - ]) - ] + ] + }) def test_tab_completion_delete(qtmodeltester, fake_web_tab, qtbot, app_stub, @@ -397,13 +397,12 @@ def test_setting_section_completion(qtmodeltester, monkeypatch, stubs): qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Sections", [ + _check_completions(model, { + "Sections": [ ('general', 'General/miscellaneous options.', ''), ('ui', 'General options related to the user interface.', ''), - ]) - ] + ] + }) def test_setting_option_completion(qtmodeltester, monkeypatch, stubs, @@ -417,14 +416,13 @@ def test_setting_option_completion(qtmodeltester, monkeypatch, stubs, qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("ui", [ + _check_completions(model, { + "ui": [ ('gesture', 'Waggle your hands to control qutebrowser', 'off'), ('mind', 'Enable mind-control ui (experimental)', 'on'), ('voice', 'Whether to respond to voice commands', 'sometimes'), - ]) - ] + ] + }) def test_setting_value_completion(qtmodeltester, monkeypatch, stubs, @@ -436,14 +434,43 @@ def test_setting_value_completion(qtmodeltester, monkeypatch, stubs, qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - actual = _get_completions(model) - assert actual == [ - ("Current/Default", [ + _check_completions(model, { + "Current/Default": [ ('0', 'Current value', ''), ('11', 'Default value', ''), - ]), - ("Completions", [ + ], + "Completions": [ ('0', '', ''), ('11', '', ''), - ]) - ] + ] + }) + + +def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub, + key_config_stub): + """Test the results of keybinding command completion. + + Validates that: + - only non-hidden and non-deprecated commands are included + - the command description is shown in the desc column + - the binding (if any) is shown in the misc column + - aliases are included + """ + _patch_cmdutils(monkeypatch, stubs, + 'qutebrowser.completion.models.miscmodels.cmdutils') + config_stub.data['aliases'] = {'rock': 'roll'} + key_config_stub.set_bindings_for('normal', {'s': 'stop', + 'rr': 'roll', + 'ro': 'rock'}) + model = miscmodels.BindCompletionModel() + qtmodeltester.data_display_may_return_none = True + qtmodeltester.check(model) + + _check_completions(model, { + "Commands": [ + ('stop', 'stop qutebrowser', 's'), + ('drop', 'drop all user data', ''), + ('hide', '', ''), + ('rock', "Alias for 'roll'", 'ro'), + ] + })