f237a87ad0
When a command has positional varargs, keep offering the configured completion for each successive argument. Right now this only influences `config-cycle`. Previously, `config-cycle <option> ` would offer a value completion for only the first argument after the option. Now it will keep offering value completion for each successive argument. This will be useful for passing multiple tags to the new bookmark commands that will be added for #882.
347 lines
13 KiB
Python
347 lines
13 KiB
Python
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
# Copyright 2016-2018 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 <http://www.gnu.org/licenses/>.
|
|
|
|
"""Tests for the Completer Object."""
|
|
|
|
import unittest.mock
|
|
|
|
import pytest
|
|
from PyQt5.QtCore import QObject
|
|
from PyQt5.QtGui import QStandardItemModel
|
|
|
|
from qutebrowser.completion import completer
|
|
from qutebrowser.commands import command, cmdutils
|
|
|
|
|
|
class FakeCompletionModel(QStandardItemModel):
|
|
|
|
"""Stub for a completion model."""
|
|
|
|
def __init__(self, kind, *pos_args, info, parent=None):
|
|
super().__init__(parent)
|
|
self.kind = kind
|
|
self.pos_args = list(pos_args)
|
|
self.info = info
|
|
|
|
|
|
class CompletionWidgetStub(QObject):
|
|
|
|
"""Stub for the CompletionView."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.hide = unittest.mock.Mock()
|
|
self.show = unittest.mock.Mock()
|
|
self.set_pattern = unittest.mock.Mock()
|
|
self.model = unittest.mock.Mock()
|
|
self.set_model = unittest.mock.Mock()
|
|
self.enabled = unittest.mock.Mock()
|
|
|
|
|
|
@pytest.fixture
|
|
def completion_widget_stub():
|
|
return CompletionWidgetStub()
|
|
|
|
|
|
@pytest.fixture
|
|
def completer_obj(qtbot, status_command_stub, config_stub, monkeypatch, stubs,
|
|
completion_widget_stub):
|
|
"""Create the completer used for testing."""
|
|
monkeypatch.setattr(completer, 'QTimer', stubs.InstaTimer)
|
|
config_stub.val.completion.show = 'auto'
|
|
return completer.Completer(cmd=status_command_stub, win_id=0,
|
|
parent=completion_widget_stub)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def miscmodels_patch(mocker):
|
|
"""Patch the miscmodels module to provide fake completion functions.
|
|
|
|
Technically some of these are not part of miscmodels, but rolling them into
|
|
one module is easier and sufficient for mocking. The only one referenced
|
|
directly by Completer is miscmodels.command.
|
|
"""
|
|
m = mocker.patch('qutebrowser.completion.completer.miscmodels',
|
|
autospec=True)
|
|
|
|
def func(name):
|
|
return lambda *args, info: FakeCompletionModel(name, *args, info=info)
|
|
|
|
m.command = func('command')
|
|
m.helptopic = func('helptopic')
|
|
m.quickmark = func('quickmark')
|
|
m.bookmark = func('bookmark')
|
|
m.session = func('session')
|
|
m.buffer = func('buffer')
|
|
m.bind = func('bind')
|
|
m.url = func('url')
|
|
m.section = func('section')
|
|
m.option = func('option')
|
|
m.value = func('value')
|
|
return m
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cmdutils_patch(monkeypatch, stubs, miscmodels_patch):
|
|
"""Patch the cmdutils module to provide fake commands."""
|
|
@cmdutils.argument('section_', completion=miscmodels_patch.section)
|
|
@cmdutils.argument('option', completion=miscmodels_patch.option)
|
|
@cmdutils.argument('value', completion=miscmodels_patch.value)
|
|
def set_command(section_=None, option=None, value=None):
|
|
"""docstring."""
|
|
pass
|
|
|
|
@cmdutils.argument('topic', completion=miscmodels_patch.helptopic)
|
|
def show_help(tab=False, bg=False, window=False, topic=None):
|
|
"""docstring."""
|
|
pass
|
|
|
|
@cmdutils.argument('url', completion=miscmodels_patch.url)
|
|
@cmdutils.argument('count', count=True)
|
|
def openurl(url=None, related=False, bg=False, tab=False, window=False,
|
|
count=None):
|
|
"""docstring."""
|
|
pass
|
|
|
|
@cmdutils.argument('win_id', win_id=True)
|
|
@cmdutils.argument('command', completion=miscmodels_patch.command)
|
|
def bind(key, win_id, command=None, *, mode='normal'):
|
|
"""docstring."""
|
|
pass
|
|
|
|
def tab_detach():
|
|
"""docstring."""
|
|
pass
|
|
|
|
@cmdutils.argument('option', completion=miscmodels_patch.option)
|
|
@cmdutils.argument('values', completion=miscmodels_patch.value)
|
|
def config_cycle(option, *values):
|
|
"""For testing varargs."""
|
|
pass
|
|
|
|
cmd_utils = stubs.FakeCmdUtils({
|
|
'set': command.Command(name='set', handler=set_command),
|
|
'help': command.Command(name='help', handler=show_help),
|
|
'open': command.Command(name='open', handler=openurl, maxsplit=0),
|
|
'bind': command.Command(name='bind', handler=bind),
|
|
'tab-detach': command.Command(name='tab-detach', handler=tab_detach),
|
|
'config-cycle': command.Command(name='config-cycle',
|
|
handler=config_cycle),
|
|
})
|
|
monkeypatch.setattr(completer, 'cmdutils', cmd_utils)
|
|
|
|
|
|
def _set_cmd_prompt(cmd, txt):
|
|
"""Set the command prompt's text and cursor position.
|
|
|
|
Args:
|
|
cmd: The command prompt object.
|
|
txt: The prompt text, using | as a placeholder for the cursor position.
|
|
"""
|
|
cmd.setText(txt.replace('|', ''))
|
|
cmd.setCursorPosition(txt.index('|'))
|
|
|
|
|
|
@pytest.mark.parametrize('txt, kind, pattern, pos_args', [
|
|
(':nope|', 'command', 'nope', []),
|
|
(':nope |', None, '', []),
|
|
(':set |', 'section', '', []),
|
|
(':set gen|', 'section', 'gen', []),
|
|
(':set general |', 'option', '', ['general']),
|
|
(':set what |', 'option', '', ['what']),
|
|
(':set general editor |', 'value', '', ['general', 'editor']),
|
|
(':set general editor gv|', 'value', 'gv', ['general', 'editor']),
|
|
(':set general editor "gvim -f"|', 'value', 'gvim -f',
|
|
['general', 'editor']),
|
|
(':set general editor "gvim |', 'value', 'gvim', ['general', 'editor']),
|
|
(':set general huh |', 'value', '', ['general', 'huh']),
|
|
(':help |', 'helptopic', '', []),
|
|
(':help |', 'helptopic', '', []),
|
|
(':open |', 'url', '', []),
|
|
(':bind |', None, '', []),
|
|
(':bind <c-x> |', 'command', '', ['<c-x>']),
|
|
(':bind <c-x> foo|', 'command', 'foo', ['<c-x>']),
|
|
(':bind <c-x>| foo', None, '<c-x>', []),
|
|
(':set| general ', 'command', 'set', []),
|
|
(':|set general ', 'command', 'set', []),
|
|
(':set gene|ral ignore-case', 'section', 'general', []),
|
|
(':|', 'command', '', []),
|
|
(': |', 'command', '', []),
|
|
('/|', None, '', []),
|
|
(':open -t|', None, '', []),
|
|
(':open --tab|', None, '', []),
|
|
(':open -t |', 'url', '', []),
|
|
(':open --tab |', 'url', '', []),
|
|
(':open | -t', 'url', '', []),
|
|
(':tab-detach |', None, '', []),
|
|
(':bind --mode=caret <c-x> |', 'command', '', ['<c-x>']),
|
|
pytest.param(':bind --mode caret <c-x> |', 'command', '', [],
|
|
marks=pytest.mark.xfail(reason='issue #74')),
|
|
(':set -t -p |', 'section', '', []),
|
|
(':open -- |', None, '', []),
|
|
(':gibberish nonesense |', None, '', []),
|
|
('/:help|', None, '', []),
|
|
('::bind|', 'command', ':bind', []),
|
|
(':-w open |', None, '', []),
|
|
# varargs
|
|
(':config-cycle option |', 'value', '', ['option']),
|
|
(':config-cycle option one |', 'value', '', ['option', 'one']),
|
|
(':config-cycle option one two |', 'value', '', ['option', 'one', 'two']),
|
|
])
|
|
def test_update_completion(txt, kind, pattern, pos_args, status_command_stub,
|
|
completer_obj, completion_widget_stub, config_stub,
|
|
key_config_stub):
|
|
"""Test setting the completion widget's model based on command text."""
|
|
# this test uses | as a placeholder for the current cursor position
|
|
_set_cmd_prompt(status_command_stub, txt)
|
|
completer_obj.schedule_completion_update()
|
|
if kind is None:
|
|
assert not completion_widget_stub.set_pattern.called
|
|
else:
|
|
assert completion_widget_stub.set_model.call_count == 1
|
|
model = completion_widget_stub.set_model.call_args[0][0]
|
|
assert model.kind == kind
|
|
assert model.pos_args == pos_args
|
|
assert model.info.config == config_stub
|
|
assert model.info.keyconf == key_config_stub
|
|
completion_widget_stub.set_pattern.assert_called_once_with(pattern)
|
|
|
|
|
|
@pytest.mark.parametrize('before, newtxt, after', [
|
|
(':|', 'set', ':set|'),
|
|
(':| ', 'set', ':set|'),
|
|
(': |', 'set', ':set|'),
|
|
(':|set', 'set', ':set|'),
|
|
(':|set ', 'set', ':set|'),
|
|
(':|se', 'set', ':set|'),
|
|
(':|se ', 'set', ':set|'),
|
|
(':s|e', 'set', ':set|'),
|
|
(':se|', 'set', ':set|'),
|
|
(':|se fonts', 'set', ':set| fonts'),
|
|
(':set |', 'fonts', ':set fonts|'),
|
|
(':set |', 'fonts', ':set fonts|'),
|
|
(':set --temp |', 'fonts', ':set --temp fonts|'),
|
|
(':set |fo', 'fonts', ':set fonts|'),
|
|
(':set f|o', 'fonts', ':set fonts|'),
|
|
(':set fo|', 'fonts', ':set fonts|'),
|
|
(':set fonts |', 'hints', ':set fonts hints|'),
|
|
(':set fonts |nt', 'hints', ':set fonts hints|'),
|
|
(':set fonts n|t', 'hints', ':set fonts hints|'),
|
|
(':set fonts nt|', 'hints', ':set fonts hints|'),
|
|
(':set | hints', 'fonts', ':set fonts| hints'),
|
|
(':set | hints', 'fonts', ':set fonts| hints'),
|
|
(':set |fo hints', 'fonts', ':set fonts| hints'),
|
|
(':set f|o hints', 'fonts', ':set fonts| hints'),
|
|
(':set fo| hints', 'fonts', ':set fonts| hints'),
|
|
(':set fonts hints |', 'Comic Sans', ":set fonts hints 'Comic Sans'|"),
|
|
(":set fonts hints 'Comic Sans'|", '12px Hack',
|
|
":set fonts hints '12px Hack'|"),
|
|
(":set fonts hints 'Comic| Sans'", '12px Hack',
|
|
":set fonts hints '12px Hack'|"),
|
|
# Make sure " is quoted properly
|
|
(':set url.start_pages \'["https://www.|example.com"]\'',
|
|
'["https://www.example.org"]',
|
|
':set url.start_pages \'["https://www.example.org"]\'|'),
|
|
# open has maxsplit=0, so treat the last two tokens as one and don't quote
|
|
(':open foo bar|', 'baz', ':open baz|'),
|
|
(':open foo| bar', 'baz', ':open baz|'),
|
|
])
|
|
def test_on_selection_changed(before, newtxt, after, completer_obj,
|
|
config_stub, status_command_stub,
|
|
completion_widget_stub):
|
|
"""Test that on_selection_changed modifies the cmd text properly.
|
|
|
|
The | represents the current cursor position in the cmd prompt.
|
|
If quick is True and there is only 1 completion (count == 1),
|
|
then we expect a space to be appended after the current word.
|
|
"""
|
|
model = unittest.mock.Mock()
|
|
completion_widget_stub.model.return_value = model
|
|
|
|
def check(quick, count, expected_txt, expected_pos):
|
|
config_stub.val.completion.quick = quick
|
|
model.count = lambda: count
|
|
_set_cmd_prompt(status_command_stub, before)
|
|
completer_obj.on_selection_changed(newtxt)
|
|
assert status_command_stub.text() == expected_txt
|
|
assert status_command_stub.cursorPosition() == expected_pos
|
|
|
|
after_pos = after.index('|')
|
|
after_txt = after.replace('|', '')
|
|
check(False, 1, after_txt, after_pos)
|
|
check(True, 2, after_txt, after_pos)
|
|
|
|
# quick-completing a single item should move the cursor ahead by 1 and add
|
|
# a trailing space if at the end of the cmd string, unless the command has
|
|
# maxsplit < len(before) (such as :open in these tests)
|
|
if after_txt.startswith(':open'):
|
|
return
|
|
|
|
after_pos += 1
|
|
if after_pos > len(after_txt):
|
|
after_txt += ' '
|
|
check(True, 1, after_txt, after_pos)
|
|
|
|
|
|
def test_quickcomplete_flicker(status_command_stub, completer_obj,
|
|
completion_widget_stub, config_stub):
|
|
"""Validate fix for #1519: bookmark-load background highlighting quirk.
|
|
|
|
For commands like bookmark-load and open with maxsplit=0, a commandline
|
|
that looks like ':open someurl |' is considered to be completing the first
|
|
arg with pattern 'someurl ' (note trailing whitespace). As this matches the
|
|
one completion available, it keeps the completionmenu open.
|
|
|
|
This test validates that the completion model is not re-set after we
|
|
quick-complete an entry after maxsplit.
|
|
"""
|
|
model = unittest.mock.Mock()
|
|
model.count = unittest.mock.Mock(return_value=1)
|
|
completion_widget_stub.model.return_value = model
|
|
config_stub.val.completion.quick = True
|
|
|
|
_set_cmd_prompt(status_command_stub, ':open |')
|
|
completer_obj.schedule_completion_update()
|
|
assert completion_widget_stub.set_model.called
|
|
completion_widget_stub.set_model.reset_mock()
|
|
|
|
# selecting a completion should not re-set the model
|
|
completer_obj.on_selection_changed('http://example.com')
|
|
completer_obj.schedule_completion_update()
|
|
assert not completion_widget_stub.set_model.called
|
|
|
|
|
|
def test_min_chars(status_command_stub, completer_obj, completion_widget_stub,
|
|
config_stub, key_config_stub):
|
|
"""Test that an update is delayed until min_chars characters are input."""
|
|
config_stub.val.completion.min_chars = 3
|
|
|
|
# Test #3635, where min_chars could crash the first update
|
|
_set_cmd_prompt(status_command_stub, ':set c|')
|
|
completer_obj.schedule_completion_update()
|
|
assert not completion_widget_stub.set_model.called
|
|
|
|
_set_cmd_prompt(status_command_stub, ':set co|')
|
|
completer_obj.schedule_completion_update()
|
|
assert not completion_widget_stub.set_model.called
|
|
|
|
_set_cmd_prompt(status_command_stub, ':set com|')
|
|
completer_obj.schedule_completion_update()
|
|
assert completion_widget_stub.set_model.call_count == 1
|