summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2021-03-18 19:12:02 +0100
committerFlorian Bruhin <me@the-compiler.org>2021-03-18 19:12:02 +0100
commitfc1a357aad19e2f919ea9b86e396fafe92434824 (patch)
treeef67df64cf200b33d25875fbc0e4af498a1378bc
parent27ad47825279a39141efd11ec9cc54ff2a872517 (diff)
parentc158cafe3395e130946cb192247e544251da8601 (diff)
downloadqutebrowser-fc1a357aad19e2f919ea9b86e396fafe92434824.tar.gz
qutebrowser-fc1a357aad19e2f919ea9b86e396fafe92434824.zip
Merge branch 'dev-split-parser'
-rw-r--r--qutebrowser/commands/parser.py209
-rw-r--r--qutebrowser/commands/runners.py197
-rw-r--r--qutebrowser/completion/completer.py24
-rw-r--r--qutebrowser/completion/models/configmodel.py8
-rw-r--r--qutebrowser/config/config.py32
-rw-r--r--qutebrowser/keyinput/modeparsers.py4
-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.py38
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):