Merge branch 'rcorre-completion_split'

This commit is contained in:
Florian Bruhin 2016-09-15 16:46:16 +02:00
commit 42aa4deecd
5 changed files with 279 additions and 355 deletions

View File

@ -174,15 +174,6 @@ class CommandRunner(QObject):
count = None
return (count, cmdstr)
def _parse_fallback(self, text, count, keep):
"""Parse the given commandline without a valid command."""
if keep:
cmdstr, sep, argstr = text.partition(' ')
cmdline = [cmdstr, sep] + argstr.split()
else:
cmdline = text.split()
return ParseResult(cmd=None, args=None, cmdline=cmdline, count=count)
def parse(self, text, *, fallback=False, keep=False):
"""Split the commandline text into command and arguments.
@ -210,7 +201,9 @@ class CommandRunner(QObject):
if not fallback:
raise cmdexc.NoSuchCommandError(
'{}: no such command'.format(cmdstr))
return self._parse_fallback(text, count, keep)
cmdline = split.split(text, keep=keep)
return ParseResult(cmd=None, args=None, cmdline=cmdline,
count=count)
args = self._split_args(cmd, argstr, keep)
if keep and args:

View File

@ -19,7 +19,7 @@
"""Completer attached to a CompletionView."""
from PyQt5.QtCore import pyqtSlot, QObject, QTimer, QItemSelection
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners
@ -36,7 +36,6 @@ class Completer(QObject):
_ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
_timer: The timer used to trigger the completion update.
_cursor_part: The cursor part index for the next completion update.
_last_cursor_pos: The old cursor position so we avoid double completion
updates.
_last_text: The old command text so we avoid double completion updates.
@ -47,16 +46,13 @@ class Completer(QObject):
self._win_id = win_id
self._cmd = cmd
self._ignore_change = False
self._empty_item_idx = None
self._timer = QTimer()
self._timer.setSingleShot(True)
self._timer.setInterval(0)
self._timer.timeout.connect(self._update_completion)
self._cursor_part = None
self._last_cursor_pos = None
self._last_text = None
self._cmd.update_completion.connect(self.schedule_completion_update)
self._cmd.textChanged.connect(self._on_text_edited)
def __repr__(self):
return utils.get_repr(self)
@ -66,23 +62,22 @@ class Completer(QObject):
completion = self.parent()
return completion.model()
def _get_completion_model(self, completion, parts, cursor_part):
def _get_completion_model(self, completion, pos_args):
"""Get a completion model based on an enum member.
Args:
completion: A usertypes.Completion member.
parts: The parts currently in the commandline.
cursor_part: The part the cursor is in.
pos_args: The positional args entered before the cursor.
Return:
A completion model or None.
"""
if completion == usertypes.Completion.option:
section = parts[cursor_part - 1]
section = pos_args[0]
model = instances.get(completion).get(section)
elif completion == usertypes.Completion.value:
section = parts[cursor_part - 2]
option = parts[cursor_part - 1]
section = pos_args[0]
option = pos_args[1]
try:
model = instances.get(completion)[section][option]
except KeyError:
@ -96,72 +91,41 @@ class Completer(QObject):
else:
return sortfilter.CompletionFilterModel(source=model, parent=self)
def _filter_cmdline_parts(self, parts, cursor_part):
"""Filter a list of commandline parts to exclude flags.
Args:
parts: A list of parts.
cursor_part: The index of the part the cursor is over.
Return:
A (parts, cursor_part) tuple with the modified values.
"""
if parts == ['']:
# Empty commandline, i.e. only :.
return [''], 0
filtered_parts = []
for i, part in enumerate(parts):
if part == '--':
break
elif part.startswith('-'):
if cursor_part >= i:
cursor_part -= 1
else:
filtered_parts.append(part)
return filtered_parts, cursor_part
def _get_new_completion(self, parts, cursor_part):
def _get_new_completion(self, before_cursor, under_cursor):
"""Get a new completion.
Args:
parts: The command chunks to get a completion for.
cursor_part: The part the cursor is over currently.
before_cursor: The command chunks before the cursor.
under_cursor: The command chunk under the cursor.
Return:
A completion model.
"""
try:
if parts[cursor_part].startswith('-'):
# cursor on a flag
return
except IndexError:
pass
log.completion.debug("Before filtering flags: parts {}, cursor_part "
"{}".format(parts, cursor_part))
parts, cursor_part = self._filter_cmdline_parts(parts, cursor_part)
log.completion.debug("After filtering flags: parts {}, cursor_part "
"{}".format(parts, cursor_part))
if not parts:
if '--' in before_cursor or under_cursor.startswith('-'):
# cursor on a flag or after an explicit split (--)
return None
if cursor_part == 0:
log.completion.debug("Before removing flags: {}".format(before_cursor))
before_cursor = [x for x in before_cursor if not x.startswith('-')]
log.completion.debug("After removing flags: {}".format(before_cursor))
if not before_cursor:
# '|' or 'set|'
model = instances.get(usertypes.Completion.command)
return sortfilter.CompletionFilterModel(source=model, parent=self)
# delegate completion to command
try:
cmd = cmdutils.cmd_dict[parts[0]]
cmd = cmdutils.cmd_dict[before_cursor[0]]
except KeyError:
# entering an unknown command
log.completion.debug("No completion for unknown command: {}"
.format(before_cursor[0]))
return None
argpos = len(before_cursor) - 1
try:
idx = cursor_part - 1
completion = cmd.get_pos_arg_info(idx).completion
completion = cmd.get_pos_arg_info(argpos).completion
except IndexError:
# user provided more positional arguments than the command takes
log.completion.debug("No completion in position {}".format(argpos))
return None
if completion is None:
return None
model = self._get_completion_model(completion, parts, cursor_part)
model = self._get_completion_model(completion, before_cursor[1:])
return model
def _quote(self, s):
@ -179,38 +143,68 @@ class Completer(QObject):
else:
return s
@pyqtSlot(QItemSelection)
def on_selection_changed(self, selected):
def _partition(self):
"""Divide the commandline text into chunks around the cursor position.
Return:
([parts_before_cursor], 'part_under_cursor', [parts_after_cursor])
"""
text = self._cmd.text()[len(self._cmd.prefix()):]
if not text or not text.strip():
# Only ":", empty part under the cursor with nothing before/after
return [], '', []
runner = runners.CommandRunner(self._win_id)
result = runner.parse(text, fallback=True, keep=True)
parts = [x for x in result.cmdline if x]
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
log.completion.debug('partitioning {} around position {}'.format(parts,
pos))
for i, part in enumerate(parts):
pos -= len(part)
if pos <= 0:
if part[pos-1:pos+1].isspace():
# cursor is in a space between two existing words
parts.insert(i, '')
prefix = [x.strip() for x in parts[:i]]
center = parts[i].strip()
# strip trailing whitepsace 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))
return prefix, center, postfix
@pyqtSlot(str)
def on_selection_changed(self, text):
"""Change the completed part if a new item was selected.
Called from the views selectionChanged method.
Args:
selected: New selection.
_deselected: Previous selection.
text: Newly selected text.
"""
indexes = selected.indexes()
if not indexes:
if text is None:
return
model = self._model()
data = model.data(indexes[0])
if data is None:
return
parts = self._split()
before, center, after = self._partition()
log.completion.debug("Changing {} to '{}'".format(center, text))
try:
needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None
except KeyError:
needs_quoting = True
if needs_quoting:
data = self._quote(data)
maxsplit = cmdutils.cmd_dict[before[0]].maxsplit
except (KeyError, IndexError):
maxsplit = None
if maxsplit is None:
text = self._quote(text)
model = self._model()
if model.count() == 1 and config.get('completion', 'quick-complete'):
# If we only have one item, we want to apply it immediately
# and go on to the next part.
self._change_completed_part(data, immediate=True)
self._change_completed_part(text, before, after, immediate=True)
if maxsplit is not None and maxsplit < len(before):
# If we are quick-completing the part after maxsplit, don't
# keep offering completions (see issue #1519)
self._ignore_change = True
else:
log.completion.debug("Will ignore next completion update.")
self._ignore_change = True
self._change_completed_part(data)
self._change_completed_part(text, before, after)
@pyqtSlot()
def schedule_completion_update(self):
@ -232,12 +226,10 @@ class Completer(QObject):
@pyqtSlot()
def _update_completion(self):
"""Check if completions are available and activate them."""
self._update_cursor_part()
parts = self._split()
before_cursor, pattern, after_cursor = self._partition()
log.completion.debug(
"Updating completion - prefix {}, parts {}, cursor_part {}".format(
self._cmd.prefix(), parts, self._cursor_part))
log.completion.debug("Updating completion: {} {} {}".format(
before_cursor, pattern, after_cursor))
if self._ignore_change:
log.completion.debug("Ignoring completion update because "
@ -255,136 +247,35 @@ class Completer(QObject):
completion.set_model(None)
return
model = self._get_new_completion(parts, self._cursor_part)
pattern = pattern.strip("'\"")
model = self._get_new_completion(before_cursor, pattern)
try:
pattern = parts[self._cursor_part].strip()
except IndexError:
pattern = ''
if model is None:
log.completion.debug("No completion model for {}.".format(parts))
else:
log.completion.debug(
"New completion for {}: {}, with pattern '{}'".format(
parts, model.srcmodel.__class__.__name__, pattern))
log.completion.debug("Setting completion model to {} with pattern '{}'"
.format(model.srcmodel.__class__.__name__ if model else 'None',
pattern))
completion.set_model(model, pattern)
def _split(self, keep=False):
"""Get the text split up in parts.
Args:
keep: Whether to keep special chars and whitespace.
aliases: Whether to resolve aliases.
"""
text = self._cmd.text()[len(self._cmd.prefix()):]
if not text:
# When only ":" is entered, we already have one imaginary part,
# which just is empty at the moment.
return ['']
if not text.strip():
# Text is only whitespace so we treat this as a single element with
# the whitespace.
return [text]
runner = runners.CommandRunner(self._win_id)
result = runner.parse(text, fallback=True, keep=keep)
parts = result.cmdline
if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, "
"inserting.".format(self._empty_item_idx))
parts.insert(self._empty_item_idx, '')
#log.completion.debug("Splitting '{}' -> {}".format(text, parts))
return parts
@pyqtSlot()
def _update_cursor_part(self):
"""Get the part index of the commandline where the cursor is over."""
cursor_pos = self._cmd.cursorPosition()
snippet = slice(cursor_pos - 1, cursor_pos + 1)
spaces = self._cmd.text()[snippet] == ' '
cursor_pos -= len(self._cmd.prefix())
parts = self._split(keep=True)
log.completion.vdebug(
"text: {}, parts: {}, cursor_pos after removing prefix '{}': "
"{}".format(self._cmd.text(), parts, self._cmd.prefix(),
cursor_pos))
skip = 0
for i, part in enumerate(parts):
log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i]))
if not part:
skip += 1
continue
if cursor_pos <= len(part):
# foo| bar
self._cursor_part = i - skip
if spaces:
self._empty_item_idx = i - skip
else:
self._empty_item_idx = None
log.completion.vdebug("cursor_pos {} <= len(part) {}, "
"setting cursor_part {} - {} (skip), "
"empty_item_idx {}".format(
cursor_pos, len(part), i, skip,
self._empty_item_idx))
break
cursor_pos -= len(part)
log.completion.vdebug(
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos))
else:
if i == 0:
# Initial `:` press without any text.
self._cursor_part = 0
else:
self._cursor_part = i - skip
if spaces:
self._empty_item_idx = i - skip
else:
self._empty_item_idx = None
log.completion.debug("cursor_part {}, spaces {}".format(
self._cursor_part, spaces))
return
def _change_completed_part(self, newtext, immediate=False):
def _change_completed_part(self, newtext, before, after, immediate=False):
"""Change the part we're currently completing in the commandline.
Args:
text: The text to set (string).
text: The text to set (string) for the token under the cursor.
before: Commandline tokens before the token under the cursor.
after: Commandline tokens after the token under the cursor.
immediate: True if the text should be completed immediately
including a trailing space and we shouldn't continue
completing the current item.
"""
parts = self._split()
log.completion.debug("changing part {} to '{}'".format(
self._cursor_part, newtext))
try:
parts[self._cursor_part] = newtext
except IndexError:
parts.append(newtext)
# We want to place the cursor directly after the part we just changed.
cursor_str = self._cmd.prefix() + ' '.join(
parts[:self._cursor_part + 1])
if immediate:
# If we should complete immediately, we want to move the cursor by
# one more char, to get to the next field.
cursor_str += ' '
text = self._cmd.prefix() + ' '.join(parts)
if immediate and self._cursor_part == len(parts) - 1:
# If we should complete immediately and we're completing the last
# part in the commandline, we automatically add a space.
text = self._cmd.prefix() + ' '.join(before + [newtext])
pos = len(text) + (1 if immediate else 0)
if after:
text += ' ' + ' '.join(after)
elif immediate:
# pad with a space if quick-completing the last entry
text += ' '
log.completion.debug("setting text = '{}', pos = {}".format(text, pos))
self._cmd.setText(text)
log.completion.debug("Placing cursor after '{}'".format(cursor_str))
log.modes.debug("Completion triggered, focusing {!r}".format(self))
self._cmd.setCursorPosition(len(cursor_str))
self._cmd.setCursorPosition(pos)
self._cmd.setFocus()
self._cmd.show_cmd.emit()
@pyqtSlot()
def _on_text_edited(self):
"""Reset _empty_item_idx if text was edited."""
self._empty_item_idx = None
# We also want to update the cursor part and emit _update_completion
# here, but that's already done for us by cursorPositionChanged
# anyways, so we don't need to do it twice.

View File

@ -24,8 +24,7 @@ subclasses to provide completions.
"""
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QItemSelectionModel,
QItemSelection)
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
from qutebrowser.config import config, style
from qutebrowser.completion import completiondelegate
@ -104,7 +103,7 @@ class CompletionView(QTreeView):
"""
resize_completion = pyqtSignal()
selection_changed = pyqtSignal(QItemSelection)
selection_changed = pyqtSignal(str)
def __init__(self, win_id, parent=None):
super().__init__(parent)
@ -310,7 +309,11 @@ class CompletionView(QTreeView):
if not self._active:
return
super().selectionChanged(selected, deselected)
self.selection_changed.emit(selected)
indexes = selected.indexes()
if not indexes:
return
data = self.model().data(indexes[0])
self.selection_changed.emit(data)
def resizeEvent(self, e):
"""Extend resizeEvent to adjust column size."""

View File

@ -82,7 +82,7 @@ def instances(monkeypatch):
}
instances[usertypes.Completion.value] = {
'general': {
'ignore-case': FakeCompletionModel(usertypes.Completion.value),
'editor': FakeCompletionModel(usertypes.Completion.value),
}
}
monkeypatch.setattr('qutebrowser.completion.completer.instances',
@ -122,16 +122,12 @@ def cmdutils_patch(monkeypatch, stubs):
"""docstring."""
pass
cmds = {
'set': set_command,
'help': show_help,
'open': openurl,
'bind': bind,
'tab-detach': tab_detach,
}
cmd_utils = stubs.FakeCmdUtils({
name: command.Command(name=name, handler=fn)
for name, fn in cmds.items()
'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),
})
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils)
@ -147,96 +143,146 @@ def _set_cmd_prompt(cmd, txt):
cmd.setCursorPosition(txt.index('|'))
def _validate_cmd_prompt(cmd, txt):
"""Interpret fake command prompt text using | as the cursor placeholder.
Args:
cmd: The command prompt object.
txt: The prompt text, using | as a placeholder for the cursor position.
"""
assert cmd.cursorPosition() == txt.index('|')
assert cmd.text() == txt.replace('|', '')
@pytest.mark.parametrize('txt, expected', [
(':nope|', usertypes.Completion.command),
(':nope |', None),
(':set |', usertypes.Completion.section),
(':set gen|', usertypes.Completion.section),
(':set general |', usertypes.Completion.option),
(':set what |', None),
(':set general ignore-case |', usertypes.Completion.value),
(':set general huh |', None),
(':help |', usertypes.Completion.helptopic),
(':help |', usertypes.Completion.helptopic),
(':open |', usertypes.Completion.url),
(':bind |', None),
(':bind <c-x> |', usertypes.Completion.command),
(':bind <c-x> foo|', usertypes.Completion.command),
(':bind <c-x>| foo', None),
(':set| general ', usertypes.Completion.command),
(':|set general ', usertypes.Completion.command),
(':set gene|ral ignore-case', usertypes.Completion.section),
(':|', usertypes.Completion.command),
(': |', usertypes.Completion.command),
('/|', None),
(':open -t|', None),
(':open --tab|', None),
(':open -t |', usertypes.Completion.url),
(':open --tab |', usertypes.Completion.url),
(':open | -t', usertypes.Completion.url),
(':--foo --bar |', None),
(':tab-detach |', None),
(':bind --mode=caret <c-x> |', usertypes.Completion.command),
@pytest.mark.parametrize('txt, kind, pattern', [
(':nope|', usertypes.Completion.command, 'nope'),
(':nope |', None, ''),
(':set |', usertypes.Completion.section, ''),
(':set gen|', usertypes.Completion.section, 'gen'),
(':set general |', usertypes.Completion.option, ''),
(':set what |', None, ''),
(':set general editor |', usertypes.Completion.value, ''),
(':set general editor gv|', usertypes.Completion.value, 'gv'),
(':set general editor "gvim -f"|', usertypes.Completion.value, 'gvim -f'),
(':set general editor "gvim |', usertypes.Completion.value, 'gvim'),
(':set general huh |', None, ''),
(':help |', usertypes.Completion.helptopic, ''),
(':help |', usertypes.Completion.helptopic, ''),
(':open |', usertypes.Completion.url, ''),
(':bind |', None, ''),
(':bind <c-x> |', usertypes.Completion.command, ''),
(':bind <c-x> foo|', usertypes.Completion.command, 'foo'),
(':bind <c-x>| foo', None, '<c-x>'),
(':set| general ', usertypes.Completion.command, 'set'),
(':|set general ', usertypes.Completion.command, 'set'),
(':set gene|ral ignore-case', usertypes.Completion.section, 'general'),
(':|', usertypes.Completion.command, ''),
(': |', usertypes.Completion.command, ''),
('/|', None, ''),
(':open -t|', None, ''),
(':open --tab|', None, ''),
(':open -t |', usertypes.Completion.url, ''),
(':open --tab |', usertypes.Completion.url, ''),
(':open | -t', usertypes.Completion.url, ''),
(':tab-detach |', None, ''),
(':bind --mode=caret <c-x> |', usertypes.Completion.command, ''),
pytest.mark.xfail(reason='issue #74')((':bind --mode caret <c-x> |',
usertypes.Completion.command)),
(':set -t -p |', usertypes.Completion.section),
(':open -- |', None),
usertypes.Completion.command, '')),
(':set -t -p |', usertypes.Completion.section, ''),
(':open -- |', None, ''),
(':gibberish nonesense |', None, ''),
])
def test_update_completion(txt, expected, status_command_stub, completer_obj,
completion_widget_stub):
def test_update_completion(txt, kind, pattern, status_command_stub,
completer_obj, completion_widget_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()
assert completion_widget_stub.set_model.call_count == 1
arg = completion_widget_stub.set_model.call_args[0][0]
args = completion_widget_stub.set_model.call_args[0]
# the outer model is just for sorting; srcmodel is the completion model
if expected is None:
assert arg == expected
if kind is None:
assert args[0] is None
else:
assert arg.srcmodel.kind == expected
assert args[0].srcmodel.kind == kind
assert args[1] == pattern
@pytest.mark.parametrize('before, newtxt, quick_complete, count, after', [
(':foo |', 'bar', False, 1, ':foo bar|'),
(':foo |', 'bar', True, 2, ':foo bar|'),
(':foo |', 'bar', True, 1, ':foo bar |'),
(':foo | bar', 'baz', False, 1, ':foo baz| bar'),
(':foo |', 'bar baz', True, 1, ":foo 'bar baz' |"),
(':foo |', '', True, 1, ":foo '' |"),
(':foo |', None, True, 1, ":foo |"),
@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'|"),
# 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, count, quick_complete, after,
completer_obj, status_command_stub,
completion_widget_stub, config_stub):
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-complete is True and there is only 1 completion (count == 1),
then we expect a space to be appended after the current word.
"""
config_stub.data['completion']['quick-complete'] = quick_complete
model = unittest.mock.Mock()
model.data = unittest.mock.Mock(return_value=newtxt)
model.count = unittest.mock.Mock(return_value=count)
indexes = [unittest.mock.Mock()]
selection = unittest.mock.Mock()
selection.indexes = unittest.mock.Mock(return_value=indexes)
completion_widget_stub.model.return_value = model
_set_cmd_prompt(status_command_stub, before)
# schedule_completion_update is needed to pick up the cursor position
def check(quick_complete, count, expected_txt, expected_pos):
config_stub.data['completion']['quick-complete'] = quick_complete
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
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.data['completion']['quick-complete'] = True
_set_cmd_prompt(status_command_stub, ':open |')
completer_obj.on_selection_changed('http://example.com')
completer_obj.schedule_completion_update()
completer_obj.on_selection_changed(selection)
model.data.assert_called_with(indexes[0])
_validate_cmd_prompt(status_command_stub, after)
assert not completion_widget_stub.set_model.called

View File

@ -96,64 +96,56 @@ def test_maybe_resize_completion(completionview, config_stub, qtbot):
completionview.maybe_resize_completion()
@pytest.mark.parametrize('which, tree, count, expected', [
('next', [['Aa']], 1, 'Aa'),
('prev', [['Aa']], 1, 'Aa'),
('next', [['Aa'], ['Ba']], 1, 'Aa'),
('prev', [['Aa'], ['Ba']], 1, 'Ba'),
('next', [['Aa'], ['Ba']], 2, 'Ba'),
('prev', [['Aa'], ['Ba']], 2, 'Aa'),
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ac'),
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ba'),
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 6, 'Ca'),
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 7, 'Aa'),
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 1, 'Ca'),
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Bb'),
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ac'),
('next', [[], ['Ba', 'Bb']], 1, 'Ba'),
('prev', [[], ['Ba', 'Bb']], 1, 'Bb'),
('next', [[], [], ['Ca', 'Cb']], 1, 'Ca'),
('prev', [[], [], ['Ca', 'Cb']], 1, 'Cb'),
('next', [['Aa'], []], 1, 'Aa'),
('prev', [['Aa'], []], 1, 'Aa'),
('next', [['Aa'], [], []], 1, 'Aa'),
('prev', [['Aa'], [], []], 1, 'Aa'),
('next', [['Aa'], [], ['Ca', 'Cb']], 2, 'Ca'),
('prev', [['Aa'], [], ['Ca', 'Cb']], 1, 'Cb'),
('next', [[]], 1, None),
('prev', [[]], 1, None),
('next-category', [['Aa']], 1, 'Aa'),
('prev-category', [['Aa']], 1, 'Aa'),
('next-category', [['Aa'], ['Ba']], 1, 'Aa'),
('prev-category', [['Aa'], ['Ba']], 1, 'Ba'),
('next-category', [['Aa'], ['Ba']], 2, 'Ba'),
('prev-category', [['Aa'], ['Ba']], 2, 'Aa'),
('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Ba'),
('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Ba'),
('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ca'),
('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Aa'),
('next-category', [[], ['Ba', 'Bb']], 1, 'Ba'),
('prev-category', [[], ['Ba', 'Bb']], 1, 'Ba'),
('next-category', [[], [], ['Ca', 'Cb']], 1, 'Ca'),
('prev-category', [[], [], ['Ca', 'Cb']], 1, 'Ca'),
('next-category', [[], [], ['Ca', 'Cb']], 2, 'Ca'),
('prev-category', [[], [], ['Ca', 'Cb']], 2, 'Ca'),
('next-category', [['Aa'], [], []], 1, 'Aa'),
('prev-category', [['Aa'], [], []], 1, 'Aa'),
('next-category', [['Aa'], [], ['Ca', 'Cb']], 2, 'Ca'),
('prev-category', [['Aa'], [], ['Ca', 'Cb']], 1, 'Ca'),
('next-category', [[]], 1, None),
('prev-category', [[]], 1, None),
@pytest.mark.parametrize('which, tree, expected', [
('next', [['Aa']], ['Aa', None, None]),
('prev', [['Aa']], ['Aa', None, None]),
('next', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']),
('prev', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']),
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Aa', 'Ab', 'Ac', 'Ba', 'Bb', 'Ca', 'Aa']),
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Ca', 'Bb', 'Ba', 'Ac', 'Ab', 'Aa', 'Ca']),
('next', [[], ['Ba', 'Bb']], ['Ba', 'Bb', 'Ba']),
('prev', [[], ['Ba', 'Bb']], ['Bb', 'Ba', 'Bb']),
('next', [[], [], ['Ca', 'Cb']], ['Ca', 'Cb', 'Ca']),
('prev', [[], [], ['Ca', 'Cb']], ['Cb', 'Ca', 'Cb']),
('next', [['Aa'], []], ['Aa', None]),
('prev', [['Aa'], []], ['Aa', None]),
('next', [['Aa'], [], []], ['Aa', None]),
('prev', [['Aa'], [], []], ['Aa', None]),
('next', [['Aa'], [], ['Ca', 'Cb']], ['Aa', 'Ca', 'Cb', 'Aa']),
('prev', [['Aa'], [], ['Ca', 'Cb']], ['Cb', 'Ca', 'Aa', 'Cb']),
('next', [[]], [None, None]),
('prev', [[]], [None, None]),
('next-category', [['Aa']], ['Aa', None, None]),
('prev-category', [['Aa']], ['Aa', None, None]),
('next-category', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']),
('prev-category', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']),
('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Aa', 'Ba', 'Ca', 'Aa']),
('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Ca', 'Ba', 'Aa', 'Ca']),
('next-category', [[], ['Ba', 'Bb']], ['Ba', None, None]),
('prev-category', [[], ['Ba', 'Bb']], ['Ba', None, None]),
('next-category', [[], [], ['Ca', 'Cb']], ['Ca', None, None]),
('prev-category', [[], [], ['Ca', 'Cb']], ['Ca', None, None]),
('next-category', [['Aa'], [], []], ['Aa', None, None]),
('prev-category', [['Aa'], [], []], ['Aa', None, None]),
('next-category', [['Aa'], [], ['Ca', 'Cb']], ['Aa', 'Ca', 'Aa']),
('prev-category', [['Aa'], [], ['Ca', 'Cb']], ['Ca', 'Aa', 'Ca']),
('next-category', [[]], [None, None]),
('prev-category', [[]], [None, None]),
])
def test_completion_item_focus(which, tree, count, expected, completionview,
qtbot):
def test_completion_item_focus(which, tree, expected, completionview, qtbot):
"""Test that on_next_prev_item moves the selection properly.
Args:
which: the direction in which to move the selection.
tree: Each list represents a completion category, with each string
being an item under that category.
count: Number of times to go forward (or back if negative).
expected: item data that should be selected after going back/forward.
expected: expected argument from on_selection_changed for each
successive movement. None implies no signal should be
emitted.
"""
model = base.BaseCompletionModel()
for catdata in tree:
@ -164,15 +156,14 @@ def test_completion_item_focus(which, tree, count, expected, completionview,
filtermodel = sortfilter.CompletionFilterModel(model,
parent=completionview)
completionview.set_model(filtermodel)
if expected is None:
for _ in range(count):
completionview.completion_item_focus(which)
else:
with qtbot.waitSignal(completionview.selection_changed):
for _ in range(count):
for entry in expected:
if entry is None:
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
idx = completionview.selectionModel().currentIndex()
assert filtermodel.data(idx) == expected
else:
with qtbot.waitSignal(completionview.selection_changed) as sig:
completionview.completion_item_focus(which)
assert sig.args == [entry]
@pytest.mark.parametrize('which', ['next', 'prev', 'next-category',