diff options
author | Florian Bruhin <me@the-compiler.org> | 2021-03-18 19:12:02 +0100 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2021-03-18 19:12:02 +0100 |
commit | fc1a357aad19e2f919ea9b86e396fafe92434824 (patch) | |
tree | ef67df64cf200b33d25875fbc0e4af498a1378bc | |
parent | 27ad47825279a39141efd11ec9cc54ff2a872517 (diff) | |
parent | c158cafe3395e130946cb192247e544251da8601 (diff) | |
download | qutebrowser-fc1a357aad19e2f919ea9b86e396fafe92434824.tar.gz qutebrowser-fc1a357aad19e2f919ea9b86e396fafe92434824.zip |
Merge branch 'dev-split-parser'
-rw-r--r-- | qutebrowser/commands/parser.py | 209 | ||||
-rw-r--r-- | qutebrowser/commands/runners.py | 197 | ||||
-rw-r--r-- | qutebrowser/completion/completer.py | 24 | ||||
-rw-r--r-- | qutebrowser/completion/models/configmodel.py | 8 | ||||
-rw-r--r-- | qutebrowser/config/config.py | 32 | ||||
-rw-r--r-- | qutebrowser/keyinput/modeparsers.py | 4 | ||||
-rw-r--r-- | tests/unit/commands/test_parser.py (renamed from tests/unit/commands/test_runners.py) | 56 | ||||
-rw-r--r-- | tests/unit/config/test_config.py | 38 |
8 files changed, 333 insertions, 235 deletions
diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py new file mode 100644 index 000000000..06a20cdf6 --- /dev/null +++ b/qutebrowser/commands/parser.py @@ -0,0 +1,209 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2021 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net> +# +# 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 <https://www.gnu.org/licenses/>. + +"""Module for parsing commands entered into the browser.""" + +import dataclasses +from typing import List, Iterator + +from qutebrowser.commands import cmdexc, command +from qutebrowser.misc import split, objects +from qutebrowser.config import config + + +@dataclasses.dataclass +class ParseResult: + + """The result of parsing a commandline.""" + + cmd: command.Command + args: List[str] + cmdline: List[str] + + +class CommandParser: + + """Parse qutebrowser commandline commands. + + Attributes: + _partial_match: Whether to allow partial command matches. + """ + + def __init__(self, partial_match: bool = False) -> None: + self._partial_match = partial_match + + def _get_alias(self, text: str, *, default: str) -> str: + """Get an alias from the config. + + Args: + text: The text to parse. + aliases: A map of aliases to commands. + default : Default value to return when alias was not found. + + Return: + The new command string if an alias was found. Default value + otherwise. + """ + parts = text.strip().split(maxsplit=1) + aliases = config.cache['aliases'] + if parts[0] not in aliases: + return default + alias = aliases[parts[0]] + + try: + new_cmd = '{} {}'.format(alias, parts[1]) + except IndexError: + new_cmd = alias + if text.endswith(' '): + new_cmd += ' ' + return new_cmd + + def _parse_all_gen( + self, + text: str, + aliases: bool = True, + **kwargs: bool, + ) -> Iterator[ParseResult]: + """Split a command on ;; and parse all parts. + + If the first command in the commandline is a non-split one, it only + returns that. + + Args: + text: Text to parse. + aliases: Whether to handle aliases. + **kwargs: Passed to parse(). + + Yields: + ParseResult tuples. + """ + text = text.strip().lstrip(':').strip() + if not text: + raise cmdexc.NoSuchCommandError("No command given") + + if aliases: + text = self._get_alias(text, default=text) + + if ';;' in text: + # Get the first command and check if it doesn't want to have ;; + # split. + first = text.split(';;')[0] + result = self.parse(first, **kwargs) + if result.cmd.no_cmd_split: + sub_texts = [text] + else: + sub_texts = [e.strip() for e in text.split(';;')] + else: + sub_texts = [text] + for sub in sub_texts: + yield self.parse(sub, **kwargs) + + def parse_all(self, text: str, **kwargs: bool) -> List[ParseResult]: + """Wrapper over _parse_all_gen.""" + return list(self._parse_all_gen(text, **kwargs)) + + def parse(self, text: str, *, keep: bool = False) -> ParseResult: + """Split the commandline text into command and arguments. + + Args: + text: Text to parse. + keep: Whether to keep special chars and whitespace. + """ + cmdstr, sep, argstr = text.partition(' ') + + if not cmdstr: + raise cmdexc.NoSuchCommandError("No command given") + + if self._partial_match: + cmdstr = self._completion_match(cmdstr) + + try: + cmd = objects.commands[cmdstr] + except KeyError: + raise cmdexc.NoSuchCommandError(f'{cmdstr}: no such command') + + args = self._split_args(cmd, argstr, keep) + if keep and args: + cmdline = [cmdstr, sep + args[0]] + args[1:] + elif keep: + cmdline = [cmdstr, sep] + else: + cmdline = [cmdstr] + args[:] + + return ParseResult(cmd=cmd, args=args, cmdline=cmdline) + + def _completion_match(self, cmdstr: str) -> str: + """Replace cmdstr with a matching completion if there's only one match. + + Args: + cmdstr: The string representing the entered command so far. + + Return: + cmdstr modified to the matching completion or unmodified + """ + matches = [cmd for cmd in sorted(objects.commands, key=len) + if cmdstr in cmd] + if len(matches) == 1: + cmdstr = matches[0] + elif len(matches) > 1 and config.val.completion.use_best_match: + cmdstr = matches[0] + return cmdstr + + def _split_args(self, cmd: command.Command, argstr: str, keep: bool) -> List[str]: + """Split the arguments from an arg string. + + Args: + cmd: The command we're currently handling. + argstr: An argument string. + keep: Whether to keep special chars and whitespace + + Return: + A list containing the split strings. + """ + if not argstr: + return [] + elif cmd.maxsplit is None: + return split.split(argstr, keep=keep) + else: + # If split=False, we still want to split the flags, but not + # everything after that. + # We first split the arg string and check the index of the first + # non-flag args, then we re-split again properly. + # example: + # + # input: "--foo -v bar baz" + # first split: ['--foo', '-v', 'bar', 'baz'] + # 0 1 2 3 + # second split: ['--foo', '-v', 'bar baz'] + # (maxsplit=2) + split_args = split.simple_split(argstr, keep=keep) + flag_arg_count = 0 + for i, arg in enumerate(split_args): + arg = arg.strip() + if arg.startswith('-'): + if arg in cmd.flags_with_args: + flag_arg_count += 1 + else: + maxsplit = i + cmd.maxsplit + flag_arg_count + return split.simple_split(argstr, keep=keep, + maxsplit=maxsplit) + + # If there are only flags, we got it right on the first try + # already. + return split_args diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 4d53295dd..5fb054455 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -22,17 +22,13 @@ import traceback import re import contextlib -import dataclasses -from typing import (TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping, - List, Optional) +from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.api import cmdutils -from qutebrowser.config import config -from qutebrowser.commands import cmdexc, command +from qutebrowser.commands import cmdexc, parser from qutebrowser.utils import message, objreg, qtutils, usertypes, utils -from qutebrowser.misc import split, objects from qutebrowser.keyinput import macros, modeman if TYPE_CHECKING: @@ -43,16 +39,6 @@ _ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str] last_command = {} -@dataclasses.dataclass -class ParseResult: - - """The result of parsing a commandline.""" - - cmd: Optional[command.Command] - args: Optional[List[str]] - cmdline: List[str] - - def _url(tabbed_browser): """Convenience method to get the current url.""" try: @@ -130,181 +116,6 @@ def replace_variables(win_id, arglist): return args -class CommandParser: - - """Parse qutebrowser commandline commands. - - Attributes: - _partial_match: Whether to allow partial command matches. - """ - - def __init__(self, partial_match=False): - self._partial_match = partial_match - - def _get_alias(self, text, default=None): - """Get an alias from the config. - - Args: - text: The text to parse. - default : Default value to return when alias was not found. - - Return: - The new command string if an alias was found. Default value - otherwise. - """ - parts = text.strip().split(maxsplit=1) - aliases = config.cache['aliases'] - if parts[0] not in aliases: - return default - alias = aliases[parts[0]] - - try: - new_cmd = '{} {}'.format(alias, parts[1]) - except IndexError: - new_cmd = alias - if text.endswith(' '): - new_cmd += ' ' - return new_cmd - - def _parse_all_gen(self, text, *args, aliases=True, **kwargs): - """Split a command on ;; and parse all parts. - - If the first command in the commandline is a non-split one, it only - returns that. - - Args: - text: Text to parse. - aliases: Whether to handle aliases. - *args/**kwargs: Passed to parse(). - - Yields: - ParseResult tuples. - """ - text = text.strip().lstrip(':').strip() - if not text: - raise cmdexc.NoSuchCommandError("No command given") - - if aliases: - text = self._get_alias(text, text) - - if ';;' in text: - # Get the first command and check if it doesn't want to have ;; - # split. - first = text.split(';;')[0] - result = self.parse(first, *args, **kwargs) - if result.cmd.no_cmd_split: - sub_texts = [text] - else: - sub_texts = [e.strip() for e in text.split(';;')] - else: - sub_texts = [text] - for sub in sub_texts: - yield self.parse(sub, *args, **kwargs) - - def parse_all(self, *args, **kwargs): - """Wrapper over _parse_all_gen.""" - return list(self._parse_all_gen(*args, **kwargs)) - - def parse(self, text, *, fallback=False, keep=False): - """Split the commandline text into command and arguments. - - Args: - text: Text to parse. - fallback: Whether to do a fallback splitting when the command was - unknown. - keep: Whether to keep special chars and whitespace - - Return: - A ParseResult tuple. - """ - cmdstr, sep, argstr = text.partition(' ') - - if not cmdstr and not fallback: - raise cmdexc.NoSuchCommandError("No command given") - - if self._partial_match: - cmdstr = self._completion_match(cmdstr) - - try: - cmd = objects.commands[cmdstr] - except KeyError: - if not fallback: - raise cmdexc.NoSuchCommandError( - '{}: no such command'.format(cmdstr)) - cmdline = split.split(text, keep=keep) - return ParseResult(cmd=None, args=None, cmdline=cmdline) - - args = self._split_args(cmd, argstr, keep) - if keep and args: - cmdline = [cmdstr, sep + args[0]] + args[1:] - elif keep: - cmdline = [cmdstr, sep] - else: - cmdline = [cmdstr] + args[:] - - return ParseResult(cmd=cmd, args=args, cmdline=cmdline) - - def _completion_match(self, cmdstr): - """Replace cmdstr with a matching completion if there's only one match. - - Args: - cmdstr: The string representing the entered command so far - - Return: - cmdstr modified to the matching completion or unmodified - """ - matches = [cmd for cmd in sorted(objects.commands, key=len) - if cmdstr in cmd] - if len(matches) == 1: - cmdstr = matches[0] - elif len(matches) > 1 and config.val.completion.use_best_match: - cmdstr = matches[0] - return cmdstr - - def _split_args(self, cmd, argstr, keep): - """Split the arguments from an arg string. - - Args: - cmd: The command we're currently handling. - argstr: An argument string. - keep: Whether to keep special chars and whitespace - - Return: - A list containing the split strings. - """ - if not argstr: - return [] - elif cmd.maxsplit is None: - return split.split(argstr, keep=keep) - else: - # If split=False, we still want to split the flags, but not - # everything after that. - # We first split the arg string and check the index of the first - # non-flag args, then we re-split again properly. - # example: - # - # input: "--foo -v bar baz" - # first split: ['--foo', '-v', 'bar', 'baz'] - # 0 1 2 3 - # second split: ['--foo', '-v', 'bar baz'] - # (maxsplit=2) - split_args = split.simple_split(argstr, keep=keep) - flag_arg_count = 0 - for i, arg in enumerate(split_args): - arg = arg.strip() - if arg.startswith('-'): - if arg in cmd.flags_with_args: - flag_arg_count += 1 - else: - maxsplit = i + cmd.maxsplit + flag_arg_count - return split.simple_split(argstr, keep=keep, - maxsplit=maxsplit) - - # If there are only flags, we got it right on the first try - # already. - return split_args - - class AbstractCommandRunner(QObject): """Abstract base class for CommandRunner.""" @@ -329,7 +140,7 @@ class CommandRunner(AbstractCommandRunner): def __init__(self, win_id, partial_match=False, parent=None): super().__init__(parent) - self._parser = CommandParser(partial_match=partial_match) + self._parser = parser.CommandParser(partial_match=partial_match) self._win_id = win_id @contextlib.contextmanager @@ -362,7 +173,7 @@ class CommandRunner(AbstractCommandRunner): parsed = self._parser.parse_all(text) if parsed is None: - return + return # type: ignore[unreachable] for result in parsed: with self._handle_error(safely): diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 52d4dc5f2..778333854 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -25,8 +25,8 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config -from qutebrowser.commands import runners -from qutebrowser.misc import objects +from qutebrowser.commands import parser, cmdexc +from qutebrowser.misc import objects, split from qutebrowser.utils import log, utils, debug, objreg from qutebrowser.completion.models import miscmodels if TYPE_CHECKING: @@ -139,13 +139,18 @@ class Completer(QObject): if not text or not text.strip(): # Only ":", empty part under the cursor with nothing before/after return [], '', [] - parser = runners.CommandParser() - result = parser.parse(text, fallback=True, keep=True) - parts = [x for x in result.cmdline if x] + + try: + parse_result = parser.CommandParser().parse(text, keep=True) + except cmdexc.NoSuchCommandError: + cmdline = split.split(text, keep=True) + else: + cmdline = parse_result.cmdline + + parts = [x for x in cmdline if x] pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars - log.completion.debug('partitioning {} around position {}'.format(parts, - pos)) + log.completion.debug(f'partitioning {parts} around position {pos}') for i, part in enumerate(parts): pos -= len(part) if pos <= 0: @@ -156,11 +161,10 @@ class Completer(QObject): center = parts[i].strip() # strip trailing whitespace included as a separate token postfix = [x.strip() for x in parts[i+1:] if not x.isspace()] - log.completion.debug( - "partitioned: {} '{}' {}".format(prefix, center, postfix)) + log.completion.debug(f"partitioned: {prefix} '{center}' {postfix}") return prefix, center, postfix - raise utils.Unreachable("Not all parts consumed: {}".format(parts)) + raise utils.Unreachable(f"Not all parts consumed: {parts}") @pyqtSlot(str) def on_selection_changed(self, text): diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index a942b868a..736d09644 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -21,7 +21,7 @@ from qutebrowser.config import configdata, configexc from qutebrowser.completion.models import completionmodel, listcategory, util -from qutebrowser.commands import runners, cmdexc +from qutebrowser.commands import parser, cmdexc from qutebrowser.keyinput import keyutils @@ -117,9 +117,8 @@ def _bind_current_default(key, info): cmd_text = info.keyconf.get_command(seq, 'normal') if cmd_text: - parser = runners.CommandParser() try: - cmd = parser.parse(cmd_text).cmd + cmd = parser.CommandParser().parse(cmd_text).cmd except cmdexc.NoSuchCommandError: data.append((cmd_text, '(Current) Invalid command!', key)) else: @@ -127,8 +126,7 @@ def _bind_current_default(key, info): cmd_text = info.keyconf.get_command(seq, 'normal', default=True) if cmd_text: - parser = runners.CommandParser() - cmd = parser.parse(cmd_text).cmd + cmd = parser.CommandParser().parse(cmd_text).cmd data.append((cmd_text, '(Default) {}'.format(cmd.desc), key)) return data diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index c644725b5..07d16ea92 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -27,6 +27,7 @@ from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping, from PyQt5.QtCore import pyqtSignal, QObject, QUrl +from qutebrowser.commands import cmdexc, parser from qutebrowser.config import configdata, configexc, configutils from qutebrowser.utils import utils, log, urlmatch from qutebrowser.misc import objects @@ -162,13 +163,38 @@ class KeyConfig: bindings[key] = binding return bindings + def _implied_cmd(self, cmdline: str) -> Optional[str]: + """Return cmdline, or the implied cmd if cmdline is a set-cmd-text.""" + try: + results = parser.CommandParser().parse_all(cmdline) + except cmdexc.NoSuchCommandError: + return None + + result = results[0] + if result.cmd.name != "set-cmd-text": + return cmdline + *flags, cmd = result.args + if "-a" in flags or "--append" in flags or not cmd.startswith(":"): + return None # doesn't look like this sets a command + return cmd.lstrip(":") + def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings': - """Get a dict of commands to a list of bindings for the mode.""" + """Get a dict of commands to a list of bindings for the mode. + + This is intented for user-facing display of keybindings. + As such, bindings for 'set-cmd-text [flags] :<cmd> ...' are translated + to '<cmd> ...', as from the user's perspective these keys behave like + bindings for '<cmd>' (that allow for further input before running). + + See #5942. + """ cmd_to_keys: KeyConfig._ReverseBindings = {} bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): - for cmd in full_cmd.split(';;'): - cmd = cmd.strip() + for cmdtext in full_cmd.split(';;'): + cmd = self._implied_cmd(cmdtext.strip()) + if not cmd: + continue cmd_to_keys.setdefault(cmd, []) # Put bindings involving modifiers last if any(info.modifiers for info in seq): diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 1a8b171c2..bd5d4e801 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -86,6 +86,8 @@ class NormalKeyParser(CommandKeyParser): _partial_timer: Timer to clear partial keypresses. """ + _sequence: keyutils.KeySequence + def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', parent: QObject = None) -> None: @@ -154,6 +156,8 @@ class HintKeyParser(basekeyparser.BaseKeyParser): _last_press: The nature of the last keypress, a LastPress member. """ + _sequence: keyutils.KeySequence + def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', hintmanager: hints.HintManager, diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_parser.py index ac9fee485..b851ad3b0 100644 --- a/tests/unit/commands/test_runners.py +++ b/tests/unit/commands/test_parser.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -"""Tests for qutebrowser.commands.runners.""" +"""Tests for qutebrowser.commands.parser.""" import pytest from qutebrowser.misc import objects -from qutebrowser.commands import runners, cmdexc +from qutebrowser.commands import parser, cmdexc class TestCommandParser: @@ -35,12 +35,12 @@ class TestCommandParser: Args: cmdline_test: A pytest fixture which provides testcases. """ - parser = runners.CommandParser() + p = parser.CommandParser() if cmdline_test.valid: - parser.parse_all(cmdline_test.cmd, aliases=False) + p.parse_all(cmdline_test.cmd, aliases=False) else: with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse_all(cmdline_test.cmd, aliases=False) + p.parse_all(cmdline_test.cmd, aliases=False) def test_parse_all_with_alias(self, cmdline_test, monkeypatch, config_stub): @@ -49,12 +49,12 @@ class TestCommandParser: config_stub.val.aliases = {'alias_name': cmdline_test.cmd} - parser = runners.CommandParser() + p = parser.CommandParser() if cmdline_test.valid: - assert len(parser.parse_all("alias_name")) > 0 + assert len(p.parse_all("alias_name")) > 0 else: with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse_all("alias_name") + p.parse_all("alias_name") @pytest.mark.parametrize('command', ['', ' ']) def test_parse_empty_with_alias(self, command): @@ -63,9 +63,33 @@ class TestCommandParser: See https://github.com/qutebrowser/qutebrowser/issues/1690 and https://github.com/qutebrowser/qutebrowser/issues/1773 """ - parser = runners.CommandParser() + p = parser.CommandParser() with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse_all(command) + p.parse_all(command) + + @pytest.mark.parametrize('command, name, args', [ + ("set-cmd-text -s :open", "set-cmd-text", ["-s", ":open"]), + ("set-cmd-text :open {url:pretty}", "set-cmd-text", + [":open {url:pretty}"]), + ("set-cmd-text -s :open -t", "set-cmd-text", ["-s", ":open -t"]), + ("set-cmd-text :open -t -r {url:pretty}", "set-cmd-text", + [":open -t -r {url:pretty}"]), + ("set-cmd-text -s :open -b", "set-cmd-text", ["-s", ":open -b"]), + ("set-cmd-text :open -b -r {url:pretty}", "set-cmd-text", + [":open -b -r {url:pretty}"]), + ("set-cmd-text -s :open -w", "set-cmd-text", + ["-s", ":open -w"]), + ("set-cmd-text :open -w {url:pretty}", "set-cmd-text", + [":open -w {url:pretty}"]), + ("set-cmd-text /", "set-cmd-text", ["/"]), + ("set-cmd-text ?", "set-cmd-text", ["?"]), + ("set-cmd-text :", "set-cmd-text", [":"]), + ]) + def test_parse_result(self, config_stub, command, name, args): + p = parser.CommandParser() + result = p.parse_all(command)[0] + assert result.cmd.name == name + assert result.args == args class TestCompletions: @@ -86,8 +110,8 @@ class TestCompletions: The same with it being disabled is tested by test_parse_all. """ - parser = runners.CommandParser(partial_match=True) - result = parser.parse('on') + p = parser.CommandParser(partial_match=True) + result = p.parse('on') assert result.cmd.name == 'one' def test_dont_use_best_match(self, config_stub): @@ -96,10 +120,10 @@ class TestCompletions: Should raise NoSuchCommandError """ config_stub.val.completion.use_best_match = False - parser = runners.CommandParser(partial_match=True) + p = parser.CommandParser(partial_match=True) with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse('tw') + p.parse('tw') def test_use_best_match(self, config_stub): """Test multiple completion options with use_best_match set to true. @@ -107,7 +131,7 @@ class TestCompletions: The resulting command should be the best match """ config_stub.val.completion.use_best_match = True - parser = runners.CommandParser(partial_match=True) + p = parser.CommandParser(partial_match=True) - result = parser.parse('tw') + result = p.parse('tw') assert result.cmd.name == 'two' diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index c28e8ce07..dd6ef54fa 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -187,17 +187,39 @@ class TestKeyConfig: @pytest.mark.parametrize('bindings, expected', [ # Simple - ({'a': 'message-info foo', 'b': 'message-info bar'}, - {'message-info foo': ['a'], 'message-info bar': ['b']}), + ({'a': 'open foo', 'b': 'open bar'}, + {'open foo': ['a'], 'open bar': ['b']}), # Multiple bindings - ({'a': 'message-info foo', 'b': 'message-info foo'}, - {'message-info foo': ['b', 'a']}), + ({'a': 'open foo', 'b': 'open foo'}, + {'open foo': ['b', 'a']}), # With modifier keys (should be listed last and normalized) - ({'a': 'message-info foo', '<ctrl-a>': 'message-info foo'}, - {'message-info foo': ['a', '<Ctrl+a>']}), + ({'a': 'open foo', '<ctrl-a>': 'open foo'}, + {'open foo': ['a', '<Ctrl+a>']}), # Chained command - ({'a': 'message-info foo ;; message-info bar'}, - {'message-info foo': ['a'], 'message-info bar': ['a']}), + ({'a': 'open foo ;; open bar'}, + {'open foo': ['a'], 'open bar': ['a']}), + # Command using set-cmd-text (#5942) + ( + { + "o": "set-cmd-text -s :open", + "O": "set-cmd-text -s :open -t", + "go": "set-cmd-text :open {url:pretty}", + # all of these should be ignored + "/": "set-cmd-text /", + "?": "set-cmd-text ?", + ":": "set-cmd-text :", + "a": "set-cmd-text no_leading_colon", + "b": "set-cmd-text -s -a :skip_cuz_append", + "c": "set-cmd-text --append :skip_cuz_append", + }, + { + "open": ["o"], + "open -t": ["O"], + "open {url:pretty}": ["go"], + } + ), + # Empty/unknown commands + ({"a": "", "b": "notreal"}, {}), ]) def test_get_reverse_bindings_for(self, key_config_stub, config_stub, no_bindings, bindings, expected): |