Merge branch 'rcorre-completion_split'
This commit is contained in:
commit
42aa4deecd
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user