Simplify update_completion.

Remove the class variables _cursor_part and _empty_item_index. Instead,
split up the commandline around the cursor whenever that information is
needed. Using locals instead of class variables makes the logic easier
to follow and ends up requiring much less code.
This commit is contained in:
Ryan Roden-Corrent 2016-09-10 21:18:59 -04:00
parent 581d7659ba
commit 127412d91c
2 changed files with 30 additions and 128 deletions

View File

@ -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.
@ -52,11 +51,9 @@ class Completer(QObject):
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 +63,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 +92,46 @@ 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.
""" """
if '--' in before_cursor:
return None
try: try:
if parts[cursor_part].startswith('-'): if under_cursor.startswith('-'):
# cursor on a flag # cursor on a flag
return return None
except IndexError: except IndexError:
pass pass
log.completion.debug("Before filtering flags: parts {}, cursor_part " log.completion.debug("Before removing flags: {}".format(before_cursor))
"{}".format(parts, cursor_part)) before_cursor = [x for x in before_cursor if not x.startswith('-')]
parts, cursor_part = self._filter_cmdline_parts(parts, cursor_part) log.completion.debug("After removing flags: {}".format(before_cursor))
log.completion.debug("After filtering flags: parts {}, cursor_part " if len(before_cursor) == 0:
"{}".format(parts, cursor_part))
if not parts:
return None
if cursor_part == 0:
# '|' 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):
@ -253,12 +223,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 "
@ -276,19 +244,11 @@ class Completer(QObject):
completion.set_model(None) completion.set_model(None)
return return
model = self._get_new_completion(parts, self._cursor_part) 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)
@ -318,55 +278,6 @@ class Completer(QObject):
#log.completion.debug("Splitting '{}' -> {}".format(text, parts)) #log.completion.debug("Splitting '{}' -> {}".format(text, parts))
return 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, immediate=False):
"""Change the part we're currently completing in the commandline. """Change the part we're currently completing in the commandline.
@ -390,11 +301,3 @@ class Completer(QObject):
self._cmd.setCursorPosition(pos) self._cmd.setCursorPosition(pos)
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.

View File

@ -174,7 +174,6 @@ def _set_cmd_prompt(cmd, txt):
(':open -t |', usertypes.Completion.url), (':open -t |', usertypes.Completion.url),
(':open --tab |', usertypes.Completion.url), (':open --tab |', usertypes.Completion.url),
(':open | -t', usertypes.Completion.url), (':open | -t', usertypes.Completion.url),
(':--foo --bar |', None),
(':tab-detach |', None), (':tab-detach |', None),
(':bind --mode=caret <c-x> |', usertypes.Completion.command), (':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> |',