Repository: qutebrowser/qutebrowser

Issue:
:bookmark-load background highlighting quirk
:bookmark-load tab highlighting requires 2 tab presses to highlight list entry. Background highlight appears to clear after first tab. Second tab highlights entry. 

Regardless, after the first tab, hitting return completes the selection.

:bookmark-load background highlighting quirk
:bookmark-load tab highlighting requires 2 tab presses to highlight list entry. Background highlight appears to clear after first tab. Second tab highlights entry. 

Regardless, after the first tab, hitting return completes the selection.



Discussion:
Can you elaborate? I get this when I press `space`, `tab` after `:bookmark-load` is selected:

![test](https://cloud.githubusercontent.com/assets/625793/15461982/a793ae58-20bf-11e6-8117-dfbd2d6e2f6b.gif)

Ah. the use case is: if the search yields only one selection, on the first tab (for say a 'gb' action), the complete url is highligted 'red' instead of a 'yellow' bg. On the second tab, the bg is turned 'yellow'. 

Maybe this is the intended behaviour. I found it confusing (until I got used to it :-)

EDIT: On a slower system I noticed that the single line item is bg highlighted 'yellow' but it immediately reverts to the dark bg with the url fg highlighted in 'red'.

I still don't understand, sorry - can you show a screenshot or ideally a gif like above (recorded via `sleep 1 && byzanz-record -h 100 -w 300 -y 668 test.gif`, adjust `-y` for your screen dimensions)?

I can reproduce this:
- Type  `:quickmark-load`
- Type some words until you have a unique match (you can even type the quickmark completely)
- Hit tab

The query will be completed to the unique match, the unique match will flash yellow, will then be displayed in a red font (as in, this whole quickmark matches the query). A second tab will select it in yellow.

Still no luck on my side:

![test](https://cloud.githubusercontent.com/assets/625793/15516620/e93eeb6c-21f3-11e6-9add-89bcf45dfccb.gif)

This is with one tab-press.

Can you try to reproduce it when doing `--temp-basedir` and adding a few quickmarks by hand?

I can reproduce the steps by @NoctuaNivalis even with `--temp-basedir`.

That's odd - can someone of you please do `:report` after reproducing the issue?

Just a sec.

Here's a desktop record of the issue, though you can't see the "flash" due to bad video quality. (The report should have arived)

[out.ogv.zip](https://github.com/The-Compiler/qutebrowser/files/280435/out.ogv.zip)

log without the issue:

```
22:34:56 DEBUG    completion completer:update_completion:346 New completion for ['quickmark-load', 'qtbu']: QuickmarkCompletionModel, with pattern 'qtbu'
22:34:56 DEBUG    modes      modeman:_eventFilter_keyrelease:214 filter: False
22:34:58 DEBUG    modes      modeman:_eventFilter_keypress:169 got keypress in mode KeyMode.command - delegating to <qutebrowser.keyinput.keyparser.PassthroughKeyParser mode='command' warn=True>
22:34:58 DEBUG    commands   command:run:488 command called: completion-item-next
22:34:58 DEBUG    commands   command:run:503 Calling qutebrowser.completion.completer.Completer.completion_item_next(<qutebrowser.completion.completer.Completer>)
22:34:58 DEBUG    completion completer:selection_changed:278 Will ignore next completion update.
22:34:58 DEBUG    completion completer:change_completed_part:441 changing part 1 to 'qtbug'
22:34:58 DEBUG    completion completer:schedule_completion_update:294 Scheduling completion update.
22:34:58 DEBUG    completion completer:schedule_completion_update:291 Ignoring update because there were no changes.
22:34:58 DEBUG    completion completer:change_completed_part:459 Placing cursor after ':quickmark-load qtbug'
22:34:58 DEBUG    modes      completer:change_completed_part:460 Completion triggered, focusing <qutebrowser.completion.completer.Completer>
22:34:58 DEBUG    modes      modeman:_eventFilter_keypress:194 handled: True, forward-unbound-keys: auto, passthrough: True, is_non_alnum: True --> filter: True (focused: <qutebrowser.mainwindow.statusbar.command.Command>)
22:34:58 DEBUG    completion completer:update_cursor_part:427 cursor_part 1, spaces False
22:34:58 DEBUG    completion completer:update_completion:307 Updating completion - prefix :, parts ['quickmark-load', 'qtbug'], cursor_part 1
22:34:58 DEBUG    completion completer:update_completion:310 Ignoring completion update because ignore_change is True.
```

log with the issue:

```
22:16:22 DEBUG    completion completer:update_completion:346 New completion for ['quickmark-load', 'ba']: QuickmarkCompletionModel, with pattern 'ba'
22:16:22 DEBUG    modes      modeman:_eventFilter_keyrelease:214 filter: False
22:16:24 DEBUG    modes      modeman:_eventFilter_keypress:169 got keypress in mode KeyMode.command - delegating to <qutebrowser.keyinput.keyparser.PassthroughKeyParser mode='command' warn=True>
22:16:24 DEBUG    commands   command:run:488 command called: completion-item-next
22:16:24 DEBUG    commands   command:run:503 Calling qutebrowser.completion.completer.Completer.completion_item_next(<qutebrowser.completion.completer.Completer>)
22:16:24 DEBUG    completion completer:change_completed_part:441 changing part 1 to 'bar'
22:16:24 DEBUG    completion completer:schedule_completion_update:294 Scheduling completion update.
22:16:24 DEBUG    completion completer:schedule_completion_update:291 Ignoring update because there were no changes.
22:16:24 DEBUG    completion completer:change_completed_part:459 Placing cursor after ':quickmark-load bar '
22:16:24 DEBUG    modes      completer:change_completed_part:460 Completion triggered, focusing <qutebrowser.completion.completer.Completer>
22:16:24 DEBUG    modes      modeman:_eventFilter_keypress:194 handled: True, forward-unbound-keys: auto, passthrough: True, is_non_alnum: True --> filter: True (focused: <qutebrowser.mainwindow.statusbar.command.Command>)
22:16:24 DEBUG    completion completer:update_cursor_part:427 cursor_part 1, spaces False
22:16:24 DEBUG    completion completer:update_completion:307 Updating completion - prefix :, parts ['quickmark-load', 'bar'], cursor_part 1
22:16:24 DEBUG    completion completer:_get_new_completion:203 Before filtering flags: parts ['quickmark-load', 'bar'], cursor_part 1
22:16:24 DEBUG    completion completer:_get_new_completion:206 After filtering flags: parts ['quickmark-load', 'bar'], cursor_part 1
22:16:24 DEBUG    completion completer:_get_new_completion:231 completions: *quickmark_by_name*
22:16:24 DEBUG    completion debug:__exit__:256 Setting filter pattern took 0.000554 seconds.
22:16:24 DEBUG    completion completer:update_completion:346 New completion for ['quickmark-load', 'bar']: QuickmarkCompletionModel, with pattern 'bar'
```

Looks like `completion completer:selection_changed:278 Will ignore next completion update.` is not logged int he second case?

Well, d'you have quick-complete on? It's on for me, and I'd think the model has only one count (match), so the `Will ignore next completion update` shouldn't be logged.

``` python
273         if model.count() == 1 and config.get('completion', 'quick-complete'):                                                                                                                                   
274             # If we only have one item, we want to apply it immediately                                                                                                                                         
275             # and go on to the next part.                                                                                                                                                                       
276             self.change_completed_part(data, immediate=True)
277         else:                                                                                                                                                                                                   
278             log.completion.debug("Will ignore next completion update.")                                                                                                                                         
279             self._ignore_change = True                                                                                                                                                                          
280             self.change_completed_part(data)
```

Doh! Reproduced now :laughing:. I thought `quick-complete` was false by default, and didn't test with `--temp-basedir` again.

This was fixed as part of #1811.

I don't think this is fixed. Start up with `--temp-basedir`, ensure `quick-complete` is `true`, and press `o``<TAB>``<TAB>`. The completion will flicker and not highlight the first time, then complete the second time. I think it is getting closed by `quick-complete`, but for some reason `set_model` is called after that with a non-`None` model which causes the completionwidget to activate again.

Ah, true. @mlochbaum or @rcorre, want to take a closer look? :wink:

After the first `TAB`, the text looks like `:open https://duckduckgo.com/ |` and 

```
(parts,self._cursor_part) = (['open', 'https://duckduckgo.com/'], 1)
```

`cursor_part` should be 2 here, I think.

I think I can fix this and clean up the `cursor_pos`/`parts` management at the same time.

Can you elaborate? I get this when I press `space`, `tab` after `:bookmark-load` is selected:

![test](https://cloud.githubusercontent.com/assets/625793/15461982/a793ae58-20bf-11e6-8117-dfbd2d6e2f6b.gif)

Ah. the use case is: if the search yields only one selection, on the first tab (for say a 'gb' action), the complete url is highligted 'red' instead of a 'yellow' bg. On the second tab, the bg is turned 'yellow'. 

Maybe this is the intended behaviour. I found it confusing (until I got used to it :-)

EDIT: On a slower system I noticed that the single line item is bg highlighted 'yellow' but it immediately reverts to the dark bg with the url fg highlighted in 'red'.

I still don't understand, sorry - can you show a screenshot or ideally a gif like above (recorded via `sleep 1 && byzanz-record -h 100 -w 300 -y 668 test.gif`, adjust `-y` for your screen dimensions)?

I can reproduce this:
- Type  `:quickmark-load`
- Type some words until you have a unique match (you can even type the quickmark completely)
- Hit tab

The query will be completed to the unique match, the unique match will flash yellow, will then be displayed in a red font (as in, this whole quickmark matches the query). A second tab will select it in yellow.

Still no luck on my side:

![test](https://cloud.githubusercontent.com/assets/625793/15516620/e93eeb6c-21f3-11e6-9add-89bcf45dfccb.gif)

This is with one tab-press.

Can you try to reproduce it when doing `--temp-basedir` and adding a few quickmarks by hand?

I can reproduce the steps by @NoctuaNivalis even with `--temp-basedir`.

That's odd - can someone of you please do `:report` after reproducing the issue?

Just a sec.

Here's a desktop record of the issue, though you can't see the "flash" due to bad video quality. (The report should have arived)

[out.ogv.zip](https://github.com/The-Compiler/qutebrowser/files/280435/out.ogv.zip)

log without the issue:

```
22:34:56 DEBUG    completion completer:update_completion:346 New completion for ['quickmark-load', 'qtbu']: QuickmarkCompletionModel, with pattern 'qtbu'
22:34:56 DEBUG    modes      modeman:_eventFilter_keyrelease:214 filter: False
22:34:58 DEBUG    modes      modeman:_eventFilter_keypress:169 got keypress in mode KeyMode.command - delegating to <qutebrowser.keyinput.keyparser.PassthroughKeyParser mode='command' warn=True>
22:34:58 DEBUG    commands   command:run:488 command called: completion-item-next
22:34:58 DEBUG    commands   command:run:503 Calling qutebrowser.completion.completer.Completer.completion_item_next(<qutebrowser.completion.completer.Completer>)
22:34:58 DEBUG    completion completer:selection_changed:278 Will ignore next completion update.
22:34:58 DEBUG    completion completer:change_completed_part:441 changing part 1 to 'qtbug'
22:34:58 DEBUG    completion completer:schedule_completion_update:294 Scheduling completion update.
22:34:58 DEBUG    completion completer:schedule_completion_update:291 Ignoring update because there were no changes.
22:34:58 DEBUG    completion completer:change_completed_part:459 Placing cursor after ':quickmark-load qtbug'
22:34:58 DEBUG    modes      completer:change_completed_part:460 Completion triggered, focusing <qutebrowser.completion.completer.Completer>
22:34:58 DEBUG    modes      modeman:_eventFilter_keypress:194 handled: True, forward-unbound-keys: auto, passthrough: True, is_non_alnum: True --> filter: True (focused: <qutebrowser.mainwindow.statusbar.command.Command>)
22:34:58 DEBUG    completion completer:update_cursor_part:427 cursor_part 1, spaces False
22:34:58 DEBUG    completion completer:update_completion:307 Updating completion - prefix :, parts ['quickmark-load', 'qtbug'], cursor_part 1
22:34:58 DEBUG    completion completer:update_completion:310 Ignoring completion update because ignore_change is True.
```

log with the issue:

```
22:16:22 DEBUG    completion completer:update_completion:346 New completion for ['quickmark-load', 'ba']: QuickmarkCompletionModel, with pattern 'ba'
22:16:22 DEBUG    modes      modeman:_eventFilter_keyrelease:214 filter: False
22:16:24 DEBUG    modes      modeman:_eventFilter_keypress:169 got keypress in mode KeyMode.command - delegating to <qutebrowser.keyinput.keyparser.PassthroughKeyParser mode='command' warn=True>
22:16:24 DEBUG    commands   command:run:488 command called: completion-item-next
22:16:24 DEBUG    commands   command:run:503 Calling qutebrowser.completion.completer.Completer.completion_item_next(<qutebrowser.completion.completer.Completer>)
22:16:24 DEBUG    completion completer:change_completed_part:441 changing part 1 to 'bar'
22:16:24 DEBUG    completion completer:schedule_completion_update:294 Scheduling completion update.
22:16:24 DEBUG    completion completer:schedule_completion_update:291 Ignoring update because there were no changes.
22:16:24 DEBUG    completion completer:change_completed_part:459 Placing cursor after ':quickmark-load bar '
22:16:24 DEBUG    modes      completer:change_completed_part:460 Completion triggered, focusing <qutebrowser.completion.completer.Completer>
22:16:24 DEBUG    modes      modeman:_eventFilter_keypress:194 handled: True, forward-unbound-keys: auto, passthrough: True, is_non_alnum: True --> filter: True (focused: <qutebrowser.mainwindow.statusbar.command.Command>)
22:16:24 DEBUG    completion completer:update_cursor_part:427 cursor_part 1, spaces False
22:16:24 DEBUG    completion completer:update_completion:307 Updating completion - prefix :, parts ['quickmark-load', 'bar'], cursor_part 1
22:16:24 DEBUG    completion completer:_get_new_completion:203 Before filtering flags: parts ['quickmark-load', 'bar'], cursor_part 1
22:16:24 DEBUG    completion completer:_get_new_completion:206 After filtering flags: parts ['quickmark-load', 'bar'], cursor_part 1
22:16:24 DEBUG    completion completer:_get_new_completion:231 completions: *quickmark_by_name*
22:16:24 DEBUG    completion debug:__exit__:256 Setting filter pattern took 0.000554 seconds.
22:16:24 DEBUG    completion completer:update_completion:346 New completion for ['quickmark-load', 'bar']: QuickmarkCompletionModel, with pattern 'bar'
```

Looks like `completion completer:selection_changed:278 Will ignore next completion update.` is not logged int he second case?

Well, d'you have quick-complete on? It's on for me, and I'd think the model has only one count (match), so the `Will ignore next completion update` shouldn't be logged.

``` python
273         if model.count() == 1 and config.get('completion', 'quick-complete'):                                                                                                                                   
274             # If we only have one item, we want to apply it immediately                                                                                                                                         
275             # and go on to the next part.                                                                                                                                                                       
276             self.change_completed_part(data, immediate=True)
277         else:                                                                                                                                                                                                   
278             log.completion.debug("Will ignore next completion update.")                                                                                                                                         
279             self._ignore_change = True                                                                                                                                                                          
280             self.change_completed_part(data)
```

Doh! Reproduced now :laughing:. I thought `quick-complete` was false by default, and didn't test with `--temp-basedir` again.

This was fixed as part of #1811.

I don't think this is fixed. Start up with `--temp-basedir`, ensure `quick-complete` is `true`, and press `o``<TAB>``<TAB>`. The completion will flicker and not highlight the first time, then complete the second time. I think it is getting closed by `quick-complete`, but for some reason `set_model` is called after that with a non-`None` model which causes the completionwidget to activate again.

Ah, true. @mlochbaum or @rcorre, want to take a closer look? :wink:

After the first `TAB`, the text looks like `:open https://duckduckgo.com/ |` and 

```
(parts,self._cursor_part) = (['open', 'https://duckduckgo.com/'], 1)
```

`cursor_part` should be 2 here, I think.

I think I can fix this and clean up the `cursor_pos`/`parts` management at the same time.


Resolving patch:
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -174,15 +174,6 @@ def _parse_count(self, cmdstr):
             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 @@ def parse(self, text, *, fallback=False, keep=False):
             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:
diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py
--- a/qutebrowser/completion/completer.py
+++ b/qutebrowser/completion/completer.py
@@ -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 @@ def __init__(self, cmd, win_id, parent=None):
         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 @@ def _model(self):
         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 @@ def _get_completion_model(self, completion, parts, cursor_part):
         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 @@ def _quote(self, s):
         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 @@ def schedule_completion_update(self):
     @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 @@ def _update_completion(self):
             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.
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -104,7 +104,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 +310,11 @@ def selectionChanged(self, selected, deselected):
         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."""


Test patch:
diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py
--- a/tests/unit/completion/test_completer.py
+++ b/tests/unit/completion/test_completer.py
@@ -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 tab_detach():
         """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
-
-
-@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 |"),
+        assert args[0].srcmodel.kind == kind
+        assert args[1] == pattern
+
+
+@pytest.mark.parametrize('before, newtxt, after', [
+    (':|', 'set', ':set|'),
+    (':| ', 'set', ':set|'),
+    (': |', 'set', ':set|'),
+    (':|set', 'set', ':set|'),
+    (':|set ', 'set', ':set|'),
+    (':|se', 'set', ':set|'),
+    (':|se ', 'set', ':set|'),
+    (':s|e', 'set', ':set|'),
+    (':se|', 'set', ':set|'),
+    (':|se fonts', 'set', ':set| fonts'),
+    (':set |', 'fonts', ':set fonts|'),
+    (':set  |', 'fonts', ':set fonts|'),
+    (':set --temp |', 'fonts', ':set --temp fonts|'),
+    (':set |fo', 'fonts', ':set fonts|'),
+    (':set f|o', 'fonts', ':set fonts|'),
+    (':set fo|', 'fonts', ':set fonts|'),
+    (':set fonts |', 'hints', ':set fonts hints|'),
+    (':set fonts |nt', 'hints', ':set fonts hints|'),
+    (':set fonts n|t', 'hints', ':set fonts hints|'),
+    (':set fonts nt|', 'hints', ':set fonts hints|'),
+    (':set | hints', 'fonts', ':set fonts| hints'),
+    (':set  |  hints', 'fonts', ':set fonts| hints'),
+    (':set |fo hints', 'fonts', ':set fonts| hints'),
+    (':set f|o hints', 'fonts', ':set fonts| hints'),
+    (':set fo| hints', 'fonts', ':set fonts| hints'),
+    (':set fonts hints |', 'Comic Sans', ":set fonts hints 'Comic Sans'|"),
+    (":set fonts hints 'Comic Sans'|", '12px Hack',
+     ":set fonts hints '12px Hack'|"),
+    (":set fonts hints 'Comic| Sans'", '12px Hack',
+     ":set fonts hints '12px Hack'|"),
+    # 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
diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py
--- a/tests/unit/completion/test_completionwidget.py
+++ b/tests/unit/completion/test_completionwidget.py
@@ -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',