Merge branch 'rcorre-completion_split'
This commit is contained in:
commit
42aa4deecd
@ -174,15 +174,6 @@ class CommandRunner(QObject):
|
|||||||
count = None
|
count = None
|
||||||
return (count, cmdstr)
|
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):
|
def parse(self, text, *, fallback=False, keep=False):
|
||||||
"""Split the commandline text into command and arguments.
|
"""Split the commandline text into command and arguments.
|
||||||
|
|
||||||
@ -210,7 +201,9 @@ class CommandRunner(QObject):
|
|||||||
if not fallback:
|
if not fallback:
|
||||||
raise cmdexc.NoSuchCommandError(
|
raise cmdexc.NoSuchCommandError(
|
||||||
'{}: no such command'.format(cmdstr))
|
'{}: 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)
|
args = self._split_args(cmd, argstr, keep)
|
||||||
if keep and args:
|
if keep and args:
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
"""Completer attached to a CompletionView."""
|
"""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.config import config
|
||||||
from qutebrowser.commands import cmdutils, runners
|
from qutebrowser.commands import cmdutils, runners
|
||||||
@ -36,7 +36,6 @@ class Completer(QObject):
|
|||||||
_ignore_change: Whether to ignore the next completion update.
|
_ignore_change: Whether to ignore the next completion update.
|
||||||
_win_id: The window ID this completer is in.
|
_win_id: The window ID this completer is in.
|
||||||
_timer: The timer used to trigger the completion update.
|
_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
|
_last_cursor_pos: The old cursor position so we avoid double completion
|
||||||
updates.
|
updates.
|
||||||
_last_text: The old command text 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._win_id = win_id
|
||||||
self._cmd = cmd
|
self._cmd = cmd
|
||||||
self._ignore_change = False
|
self._ignore_change = False
|
||||||
self._empty_item_idx = None
|
|
||||||
self._timer = QTimer()
|
self._timer = QTimer()
|
||||||
self._timer.setSingleShot(True)
|
self._timer.setSingleShot(True)
|
||||||
self._timer.setInterval(0)
|
self._timer.setInterval(0)
|
||||||
self._timer.timeout.connect(self._update_completion)
|
self._timer.timeout.connect(self._update_completion)
|
||||||
self._cursor_part = None
|
|
||||||
self._last_cursor_pos = None
|
self._last_cursor_pos = None
|
||||||
self._last_text = None
|
self._last_text = None
|
||||||
self._cmd.update_completion.connect(self.schedule_completion_update)
|
self._cmd.update_completion.connect(self.schedule_completion_update)
|
||||||
self._cmd.textChanged.connect(self._on_text_edited)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self)
|
return utils.get_repr(self)
|
||||||
@ -66,23 +62,22 @@ class Completer(QObject):
|
|||||||
completion = self.parent()
|
completion = self.parent()
|
||||||
return completion.model()
|
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.
|
"""Get a completion model based on an enum member.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
completion: A usertypes.Completion member.
|
completion: A usertypes.Completion member.
|
||||||
parts: The parts currently in the commandline.
|
pos_args: The positional args entered before the cursor.
|
||||||
cursor_part: The part the cursor is in.
|
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A completion model or None.
|
A completion model or None.
|
||||||
"""
|
"""
|
||||||
if completion == usertypes.Completion.option:
|
if completion == usertypes.Completion.option:
|
||||||
section = parts[cursor_part - 1]
|
section = pos_args[0]
|
||||||
model = instances.get(completion).get(section)
|
model = instances.get(completion).get(section)
|
||||||
elif completion == usertypes.Completion.value:
|
elif completion == usertypes.Completion.value:
|
||||||
section = parts[cursor_part - 2]
|
section = pos_args[0]
|
||||||
option = parts[cursor_part - 1]
|
option = pos_args[1]
|
||||||
try:
|
try:
|
||||||
model = instances.get(completion)[section][option]
|
model = instances.get(completion)[section][option]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -96,72 +91,41 @@ class Completer(QObject):
|
|||||||
else:
|
else:
|
||||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||||
|
|
||||||
def _filter_cmdline_parts(self, parts, cursor_part):
|
def _get_new_completion(self, before_cursor, under_cursor):
|
||||||
"""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):
|
|
||||||
"""Get a new completion.
|
"""Get a new completion.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parts: The command chunks to get a completion for.
|
before_cursor: The command chunks before the cursor.
|
||||||
cursor_part: The part the cursor is over currently.
|
under_cursor: The command chunk under the cursor.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A completion model.
|
A completion model.
|
||||||
"""
|
"""
|
||||||
try:
|
if '--' in before_cursor or under_cursor.startswith('-'):
|
||||||
if parts[cursor_part].startswith('-'):
|
# cursor on a flag or after an explicit split (--)
|
||||||
# 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:
|
|
||||||
return None
|
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|'
|
# '|' or 'set|'
|
||||||
model = instances.get(usertypes.Completion.command)
|
model = instances.get(usertypes.Completion.command)
|
||||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||||
# delegate completion to command
|
|
||||||
try:
|
try:
|
||||||
cmd = cmdutils.cmd_dict[parts[0]]
|
cmd = cmdutils.cmd_dict[before_cursor[0]]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# entering an unknown command
|
log.completion.debug("No completion for unknown command: {}"
|
||||||
|
.format(before_cursor[0]))
|
||||||
return None
|
return None
|
||||||
|
argpos = len(before_cursor) - 1
|
||||||
try:
|
try:
|
||||||
idx = cursor_part - 1
|
completion = cmd.get_pos_arg_info(argpos).completion
|
||||||
completion = cmd.get_pos_arg_info(idx).completion
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# user provided more positional arguments than the command takes
|
log.completion.debug("No completion in position {}".format(argpos))
|
||||||
return None
|
return None
|
||||||
if completion is None:
|
if completion is None:
|
||||||
return None
|
return None
|
||||||
model = self._get_completion_model(completion, parts, cursor_part)
|
model = self._get_completion_model(completion, before_cursor[1:])
|
||||||
return model
|
return model
|
||||||
|
|
||||||
def _quote(self, s):
|
def _quote(self, s):
|
||||||
@ -179,38 +143,68 @@ class Completer(QObject):
|
|||||||
else:
|
else:
|
||||||
return s
|
return s
|
||||||
|
|
||||||
@pyqtSlot(QItemSelection)
|
def _partition(self):
|
||||||
def on_selection_changed(self, selected):
|
"""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.
|
"""Change the completed part if a new item was selected.
|
||||||
|
|
||||||
Called from the views selectionChanged method.
|
Called from the views selectionChanged method.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
selected: New selection.
|
text: Newly selected text.
|
||||||
_deselected: Previous selection.
|
|
||||||
"""
|
"""
|
||||||
indexes = selected.indexes()
|
if text is None:
|
||||||
if not indexes:
|
|
||||||
return
|
return
|
||||||
model = self._model()
|
before, center, after = self._partition()
|
||||||
data = model.data(indexes[0])
|
log.completion.debug("Changing {} to '{}'".format(center, text))
|
||||||
if data is None:
|
|
||||||
return
|
|
||||||
parts = self._split()
|
|
||||||
try:
|
try:
|
||||||
needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None
|
maxsplit = cmdutils.cmd_dict[before[0]].maxsplit
|
||||||
except KeyError:
|
except (KeyError, IndexError):
|
||||||
needs_quoting = True
|
maxsplit = None
|
||||||
if needs_quoting:
|
if maxsplit is None:
|
||||||
data = self._quote(data)
|
text = self._quote(text)
|
||||||
|
model = self._model()
|
||||||
if model.count() == 1 and config.get('completion', 'quick-complete'):
|
if model.count() == 1 and config.get('completion', 'quick-complete'):
|
||||||
# If we only have one item, we want to apply it immediately
|
# If we only have one item, we want to apply it immediately
|
||||||
# and go on to the next part.
|
# 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:
|
else:
|
||||||
log.completion.debug("Will ignore next completion update.")
|
log.completion.debug("Will ignore next completion update.")
|
||||||
self._ignore_change = True
|
self._ignore_change = True
|
||||||
self._change_completed_part(data)
|
self._change_completed_part(text, before, after)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def schedule_completion_update(self):
|
def schedule_completion_update(self):
|
||||||
@ -232,12 +226,10 @@ class Completer(QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _update_completion(self):
|
def _update_completion(self):
|
||||||
"""Check if completions are available and activate them."""
|
"""Check if completions are available and activate them."""
|
||||||
self._update_cursor_part()
|
before_cursor, pattern, after_cursor = self._partition()
|
||||||
parts = self._split()
|
|
||||||
|
|
||||||
log.completion.debug(
|
log.completion.debug("Updating completion: {} {} {}".format(
|
||||||
"Updating completion - prefix {}, parts {}, cursor_part {}".format(
|
before_cursor, pattern, after_cursor))
|
||||||
self._cmd.prefix(), parts, self._cursor_part))
|
|
||||||
|
|
||||||
if self._ignore_change:
|
if self._ignore_change:
|
||||||
log.completion.debug("Ignoring completion update because "
|
log.completion.debug("Ignoring completion update because "
|
||||||
@ -255,136 +247,35 @@ class Completer(QObject):
|
|||||||
completion.set_model(None)
|
completion.set_model(None)
|
||||||
return
|
return
|
||||||
|
|
||||||
model = self._get_new_completion(parts, self._cursor_part)
|
pattern = pattern.strip("'\"")
|
||||||
|
model = self._get_new_completion(before_cursor, pattern)
|
||||||
|
|
||||||
try:
|
log.completion.debug("Setting completion model to {} with pattern '{}'"
|
||||||
pattern = parts[self._cursor_part].strip()
|
.format(model.srcmodel.__class__.__name__ if model else 'None',
|
||||||
except IndexError:
|
pattern))
|
||||||
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))
|
|
||||||
|
|
||||||
completion.set_model(model, pattern)
|
completion.set_model(model, pattern)
|
||||||
|
|
||||||
def _split(self, keep=False):
|
def _change_completed_part(self, newtext, before, after, immediate=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):
|
|
||||||
"""Change the part we're currently completing in the commandline.
|
"""Change the part we're currently completing in the commandline.
|
||||||
|
|
||||||
Args:
|
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
|
immediate: True if the text should be completed immediately
|
||||||
including a trailing space and we shouldn't continue
|
including a trailing space and we shouldn't continue
|
||||||
completing the current item.
|
completing the current item.
|
||||||
"""
|
"""
|
||||||
parts = self._split()
|
text = self._cmd.prefix() + ' '.join(before + [newtext])
|
||||||
log.completion.debug("changing part {} to '{}'".format(
|
pos = len(text) + (1 if immediate else 0)
|
||||||
self._cursor_part, newtext))
|
if after:
|
||||||
try:
|
text += ' ' + ' '.join(after)
|
||||||
parts[self._cursor_part] = newtext
|
elif immediate:
|
||||||
except IndexError:
|
# pad with a space if quick-completing the last entry
|
||||||
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 += ' '
|
text += ' '
|
||||||
|
log.completion.debug("setting text = '{}', pos = {}".format(text, pos))
|
||||||
self._cmd.setText(text)
|
self._cmd.setText(text)
|
||||||
log.completion.debug("Placing cursor after '{}'".format(cursor_str))
|
self._cmd.setCursorPosition(pos)
|
||||||
log.modes.debug("Completion triggered, focusing {!r}".format(self))
|
|
||||||
self._cmd.setCursorPosition(len(cursor_str))
|
|
||||||
self._cmd.setFocus()
|
self._cmd.setFocus()
|
||||||
self._cmd.show_cmd.emit()
|
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.
|
|
||||||
|
@ -24,8 +24,7 @@ subclasses to provide completions.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
|
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
|
||||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QItemSelectionModel,
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
|
||||||
QItemSelection)
|
|
||||||
|
|
||||||
from qutebrowser.config import config, style
|
from qutebrowser.config import config, style
|
||||||
from qutebrowser.completion import completiondelegate
|
from qutebrowser.completion import completiondelegate
|
||||||
@ -104,7 +103,7 @@ class CompletionView(QTreeView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
resize_completion = pyqtSignal()
|
resize_completion = pyqtSignal()
|
||||||
selection_changed = pyqtSignal(QItemSelection)
|
selection_changed = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, win_id, parent=None):
|
def __init__(self, win_id, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -310,7 +309,11 @@ class CompletionView(QTreeView):
|
|||||||
if not self._active:
|
if not self._active:
|
||||||
return
|
return
|
||||||
super().selectionChanged(selected, deselected)
|
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):
|
def resizeEvent(self, e):
|
||||||
"""Extend resizeEvent to adjust column size."""
|
"""Extend resizeEvent to adjust column size."""
|
||||||
|
@ -82,7 +82,7 @@ def instances(monkeypatch):
|
|||||||
}
|
}
|
||||||
instances[usertypes.Completion.value] = {
|
instances[usertypes.Completion.value] = {
|
||||||
'general': {
|
'general': {
|
||||||
'ignore-case': FakeCompletionModel(usertypes.Completion.value),
|
'editor': FakeCompletionModel(usertypes.Completion.value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
monkeypatch.setattr('qutebrowser.completion.completer.instances',
|
monkeypatch.setattr('qutebrowser.completion.completer.instances',
|
||||||
@ -122,16 +122,12 @@ def cmdutils_patch(monkeypatch, stubs):
|
|||||||
"""docstring."""
|
"""docstring."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cmds = {
|
|
||||||
'set': set_command,
|
|
||||||
'help': show_help,
|
|
||||||
'open': openurl,
|
|
||||||
'bind': bind,
|
|
||||||
'tab-detach': tab_detach,
|
|
||||||
}
|
|
||||||
cmd_utils = stubs.FakeCmdUtils({
|
cmd_utils = stubs.FakeCmdUtils({
|
||||||
name: command.Command(name=name, handler=fn)
|
'set': command.Command(name='set', handler=set_command),
|
||||||
for name, fn in cmds.items()
|
'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)
|
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils)
|
||||||
|
|
||||||
@ -147,96 +143,146 @@ def _set_cmd_prompt(cmd, txt):
|
|||||||
cmd.setCursorPosition(txt.index('|'))
|
cmd.setCursorPosition(txt.index('|'))
|
||||||
|
|
||||||
|
|
||||||
def _validate_cmd_prompt(cmd, txt):
|
@pytest.mark.parametrize('txt, kind, pattern', [
|
||||||
"""Interpret fake command prompt text using | as the cursor placeholder.
|
(':nope|', usertypes.Completion.command, 'nope'),
|
||||||
|
(':nope |', None, ''),
|
||||||
Args:
|
(':set |', usertypes.Completion.section, ''),
|
||||||
cmd: The command prompt object.
|
(':set gen|', usertypes.Completion.section, 'gen'),
|
||||||
txt: The prompt text, using | as a placeholder for the cursor position.
|
(':set general |', usertypes.Completion.option, ''),
|
||||||
"""
|
(':set what |', None, ''),
|
||||||
assert cmd.cursorPosition() == txt.index('|')
|
(':set general editor |', usertypes.Completion.value, ''),
|
||||||
assert cmd.text() == txt.replace('|', '')
|
(':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'),
|
||||||
@pytest.mark.parametrize('txt, expected', [
|
(':set general huh |', None, ''),
|
||||||
(':nope|', usertypes.Completion.command),
|
(':help |', usertypes.Completion.helptopic, ''),
|
||||||
(':nope |', None),
|
(':help |', usertypes.Completion.helptopic, ''),
|
||||||
(':set |', usertypes.Completion.section),
|
(':open |', usertypes.Completion.url, ''),
|
||||||
(':set gen|', usertypes.Completion.section),
|
(':bind |', None, ''),
|
||||||
(':set general |', usertypes.Completion.option),
|
(':bind <c-x> |', usertypes.Completion.command, ''),
|
||||||
(':set what |', None),
|
(':bind <c-x> foo|', usertypes.Completion.command, 'foo'),
|
||||||
(':set general ignore-case |', usertypes.Completion.value),
|
(':bind <c-x>| foo', None, '<c-x>'),
|
||||||
(':set general huh |', None),
|
(':set| general ', usertypes.Completion.command, 'set'),
|
||||||
(':help |', usertypes.Completion.helptopic),
|
(':|set general ', usertypes.Completion.command, 'set'),
|
||||||
(':help |', usertypes.Completion.helptopic),
|
(':set gene|ral ignore-case', usertypes.Completion.section, 'general'),
|
||||||
(':open |', usertypes.Completion.url),
|
(':|', usertypes.Completion.command, ''),
|
||||||
(':bind |', None),
|
(': |', usertypes.Completion.command, ''),
|
||||||
(':bind <c-x> |', usertypes.Completion.command),
|
('/|', None, ''),
|
||||||
(':bind <c-x> foo|', usertypes.Completion.command),
|
(':open -t|', None, ''),
|
||||||
(':bind <c-x>| foo', None),
|
(':open --tab|', None, ''),
|
||||||
(':set| general ', usertypes.Completion.command),
|
(':open -t |', usertypes.Completion.url, ''),
|
||||||
(':|set general ', usertypes.Completion.command),
|
(':open --tab |', usertypes.Completion.url, ''),
|
||||||
(':set gene|ral ignore-case', usertypes.Completion.section),
|
(':open | -t', usertypes.Completion.url, ''),
|
||||||
(':|', usertypes.Completion.command),
|
(':tab-detach |', None, ''),
|
||||||
(': |', usertypes.Completion.command),
|
(':bind --mode=caret <c-x> |', 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.xfail(reason='issue #74')((':bind --mode caret <c-x> |',
|
pytest.mark.xfail(reason='issue #74')((':bind --mode caret <c-x> |',
|
||||||
usertypes.Completion.command)),
|
usertypes.Completion.command, '')),
|
||||||
(':set -t -p |', usertypes.Completion.section),
|
(':set -t -p |', usertypes.Completion.section, ''),
|
||||||
(':open -- |', None),
|
(':open -- |', None, ''),
|
||||||
|
(':gibberish nonesense |', None, ''),
|
||||||
])
|
])
|
||||||
def test_update_completion(txt, expected, status_command_stub, completer_obj,
|
def test_update_completion(txt, kind, pattern, status_command_stub,
|
||||||
completion_widget_stub):
|
completer_obj, completion_widget_stub):
|
||||||
"""Test setting the completion widget's model based on command text."""
|
"""Test setting the completion widget's model based on command text."""
|
||||||
# this test uses | as a placeholder for the current cursor position
|
# this test uses | as a placeholder for the current cursor position
|
||||||
_set_cmd_prompt(status_command_stub, txt)
|
_set_cmd_prompt(status_command_stub, txt)
|
||||||
completer_obj.schedule_completion_update()
|
completer_obj.schedule_completion_update()
|
||||||
assert completion_widget_stub.set_model.call_count == 1
|
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
|
# the outer model is just for sorting; srcmodel is the completion model
|
||||||
if expected is None:
|
if kind is None:
|
||||||
assert arg == expected
|
assert args[0] is None
|
||||||
else:
|
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', [
|
@pytest.mark.parametrize('before, newtxt, after', [
|
||||||
(':foo |', 'bar', False, 1, ':foo bar|'),
|
(':|', 'set', ':set|'),
|
||||||
(':foo |', 'bar', True, 2, ':foo bar|'),
|
(':| ', 'set', ':set|'),
|
||||||
(':foo |', 'bar', True, 1, ':foo bar |'),
|
(': |', 'set', ':set|'),
|
||||||
(':foo | bar', 'baz', False, 1, ':foo baz| bar'),
|
(':|set', 'set', ':set|'),
|
||||||
(':foo |', 'bar baz', True, 1, ":foo 'bar baz' |"),
|
(':|set ', 'set', ':set|'),
|
||||||
(':foo |', '', True, 1, ":foo '' |"),
|
(':|se', 'set', ':set|'),
|
||||||
(':foo |', None, True, 1, ":foo |"),
|
(':|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,
|
def test_on_selection_changed(before, newtxt, after, completer_obj,
|
||||||
completer_obj, status_command_stub,
|
config_stub, status_command_stub,
|
||||||
completion_widget_stub, config_stub):
|
completion_widget_stub):
|
||||||
"""Test that on_selection_changed modifies the cmd text properly.
|
"""Test that on_selection_changed modifies the cmd text properly.
|
||||||
|
|
||||||
The | represents the current cursor position in the cmd prompt.
|
The | represents the current cursor position in the cmd prompt.
|
||||||
If quick-complete is True and there is only 1 completion (count == 1),
|
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.
|
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 = 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
|
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.schedule_completion_update()
|
||||||
completer_obj.on_selection_changed(selection)
|
assert not completion_widget_stub.set_model.called
|
||||||
model.data.assert_called_with(indexes[0])
|
|
||||||
_validate_cmd_prompt(status_command_stub, after)
|
|
||||||
|
@ -96,64 +96,56 @@ def test_maybe_resize_completion(completionview, config_stub, qtbot):
|
|||||||
completionview.maybe_resize_completion()
|
completionview.maybe_resize_completion()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('which, tree, count, expected', [
|
@pytest.mark.parametrize('which, tree, expected', [
|
||||||
('next', [['Aa']], 1, 'Aa'),
|
('next', [['Aa']], ['Aa', None, None]),
|
||||||
('prev', [['Aa']], 1, 'Aa'),
|
('prev', [['Aa']], ['Aa', None, None]),
|
||||||
('next', [['Aa'], ['Ba']], 1, 'Aa'),
|
('next', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']),
|
||||||
('prev', [['Aa'], ['Ba']], 1, 'Ba'),
|
('prev', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']),
|
||||||
('next', [['Aa'], ['Ba']], 2, 'Ba'),
|
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
|
||||||
('prev', [['Aa'], ['Ba']], 2, 'Aa'),
|
['Aa', 'Ab', 'Ac', 'Ba', 'Bb', 'Ca', 'Aa']),
|
||||||
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ac'),
|
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
|
||||||
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ba'),
|
['Ca', 'Bb', 'Ba', 'Ac', 'Ab', 'Aa', 'Ca']),
|
||||||
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 6, 'Ca'),
|
('next', [[], ['Ba', 'Bb']], ['Ba', 'Bb', 'Ba']),
|
||||||
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 7, 'Aa'),
|
('prev', [[], ['Ba', 'Bb']], ['Bb', 'Ba', 'Bb']),
|
||||||
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 1, 'Ca'),
|
('next', [[], [], ['Ca', 'Cb']], ['Ca', 'Cb', 'Ca']),
|
||||||
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Bb'),
|
('prev', [[], [], ['Ca', 'Cb']], ['Cb', 'Ca', 'Cb']),
|
||||||
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ac'),
|
('next', [['Aa'], []], ['Aa', None]),
|
||||||
('next', [[], ['Ba', 'Bb']], 1, 'Ba'),
|
('prev', [['Aa'], []], ['Aa', None]),
|
||||||
('prev', [[], ['Ba', 'Bb']], 1, 'Bb'),
|
('next', [['Aa'], [], []], ['Aa', None]),
|
||||||
('next', [[], [], ['Ca', 'Cb']], 1, 'Ca'),
|
('prev', [['Aa'], [], []], ['Aa', None]),
|
||||||
('prev', [[], [], ['Ca', 'Cb']], 1, 'Cb'),
|
('next', [['Aa'], [], ['Ca', 'Cb']], ['Aa', 'Ca', 'Cb', 'Aa']),
|
||||||
('next', [['Aa'], []], 1, 'Aa'),
|
('prev', [['Aa'], [], ['Ca', 'Cb']], ['Cb', 'Ca', 'Aa', 'Cb']),
|
||||||
('prev', [['Aa'], []], 1, 'Aa'),
|
('next', [[]], [None, None]),
|
||||||
('next', [['Aa'], [], []], 1, 'Aa'),
|
('prev', [[]], [None, None]),
|
||||||
('prev', [['Aa'], [], []], 1, 'Aa'),
|
('next-category', [['Aa']], ['Aa', None, None]),
|
||||||
('next', [['Aa'], [], ['Ca', 'Cb']], 2, 'Ca'),
|
('prev-category', [['Aa']], ['Aa', None, None]),
|
||||||
('prev', [['Aa'], [], ['Ca', 'Cb']], 1, 'Cb'),
|
('next-category', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']),
|
||||||
('next', [[]], 1, None),
|
('prev-category', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']),
|
||||||
('prev', [[]], 1, None),
|
('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
|
||||||
('next-category', [['Aa']], 1, 'Aa'),
|
['Aa', 'Ba', 'Ca', 'Aa']),
|
||||||
('prev-category', [['Aa']], 1, 'Aa'),
|
('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
|
||||||
('next-category', [['Aa'], ['Ba']], 1, 'Aa'),
|
['Ca', 'Ba', 'Aa', 'Ca']),
|
||||||
('prev-category', [['Aa'], ['Ba']], 1, 'Ba'),
|
('next-category', [[], ['Ba', 'Bb']], ['Ba', None, None]),
|
||||||
('next-category', [['Aa'], ['Ba']], 2, 'Ba'),
|
('prev-category', [[], ['Ba', 'Bb']], ['Ba', None, None]),
|
||||||
('prev-category', [['Aa'], ['Ba']], 2, 'Aa'),
|
('next-category', [[], [], ['Ca', 'Cb']], ['Ca', None, None]),
|
||||||
('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Ba'),
|
('prev-category', [[], [], ['Ca', 'Cb']], ['Ca', None, None]),
|
||||||
('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 2, 'Ba'),
|
('next-category', [['Aa'], [], []], ['Aa', None, None]),
|
||||||
('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ca'),
|
('prev-category', [['Aa'], [], []], ['Aa', None, None]),
|
||||||
('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Aa'),
|
('next-category', [['Aa'], [], ['Ca', 'Cb']], ['Aa', 'Ca', 'Aa']),
|
||||||
('next-category', [[], ['Ba', 'Bb']], 1, 'Ba'),
|
('prev-category', [['Aa'], [], ['Ca', 'Cb']], ['Ca', 'Aa', 'Ca']),
|
||||||
('prev-category', [[], ['Ba', 'Bb']], 1, 'Ba'),
|
('next-category', [[]], [None, None]),
|
||||||
('next-category', [[], [], ['Ca', 'Cb']], 1, 'Ca'),
|
('prev-category', [[]], [None, None]),
|
||||||
('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),
|
|
||||||
])
|
])
|
||||||
def test_completion_item_focus(which, tree, count, expected, completionview,
|
def test_completion_item_focus(which, tree, expected, completionview, qtbot):
|
||||||
qtbot):
|
|
||||||
"""Test that on_next_prev_item moves the selection properly.
|
"""Test that on_next_prev_item moves the selection properly.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
which: the direction in which to move the selection.
|
||||||
tree: Each list represents a completion category, with each string
|
tree: Each list represents a completion category, with each string
|
||||||
being an item under that category.
|
being an item under that category.
|
||||||
count: Number of times to go forward (or back if negative).
|
expected: expected argument from on_selection_changed for each
|
||||||
expected: item data that should be selected after going back/forward.
|
successive movement. None implies no signal should be
|
||||||
|
emitted.
|
||||||
"""
|
"""
|
||||||
model = base.BaseCompletionModel()
|
model = base.BaseCompletionModel()
|
||||||
for catdata in tree:
|
for catdata in tree:
|
||||||
@ -164,15 +156,14 @@ def test_completion_item_focus(which, tree, count, expected, completionview,
|
|||||||
filtermodel = sortfilter.CompletionFilterModel(model,
|
filtermodel = sortfilter.CompletionFilterModel(model,
|
||||||
parent=completionview)
|
parent=completionview)
|
||||||
completionview.set_model(filtermodel)
|
completionview.set_model(filtermodel)
|
||||||
if expected is None:
|
for entry in expected:
|
||||||
for _ in range(count):
|
if entry is None:
|
||||||
completionview.completion_item_focus(which)
|
with qtbot.assertNotEmitted(completionview.selection_changed):
|
||||||
else:
|
|
||||||
with qtbot.waitSignal(completionview.selection_changed):
|
|
||||||
for _ in range(count):
|
|
||||||
completionview.completion_item_focus(which)
|
completionview.completion_item_focus(which)
|
||||||
idx = completionview.selectionModel().currentIndex()
|
else:
|
||||||
assert filtermodel.data(idx) == expected
|
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',
|
@pytest.mark.parametrize('which', ['next', 'prev', 'next-category',
|
||||||
|
Loading…
Reference in New Issue
Block a user