Merge https://github.com/The-Compiler/qutebrowser into safe-args
This commit is contained in:
commit
cfd166a95e
@ -32,6 +32,12 @@ Added
|
||||
in rapid mode.
|
||||
- New `{clipboard}` and `{primary}` replacements for the commandline which
|
||||
replace the `:paste` command.
|
||||
- New `:insert-text` command to insert a given text into a field on the page,
|
||||
which replaces `:paste-primary` together with the `{primary}` replacement.
|
||||
- New `:window-only` command to close all other windows.
|
||||
- New `prev-category` and `next-category` arguments to `:completion-item-focus`
|
||||
to focus the previous/next category in the completion (bound to `<Ctrl-Tab>`
|
||||
and `<Ctrl-Shift-Tab>` by default).
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -66,12 +72,18 @@ Changed
|
||||
- Completions for `:help` and `:bind` now also show hidden commands
|
||||
- The `:buffer` completion now also filters using the first column (id).
|
||||
- `:undo` has been improved to reopen tabs at the position they were closed.
|
||||
- `:navigate` now takes a count for `up`/`increment`/`decrement`.
|
||||
- The `hints -> auto-follow` setting now can be set to
|
||||
`always`/`full-match`/`unique-match`/`never` to more precisely control when
|
||||
hints should be followed automatically.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- The `:paste` command got deprecated as `:open` with `{clipboard}` and
|
||||
`{primary}` can be used instead.
|
||||
- The `:paste-primary` command got deprecated as `:insert-text {primary}` can
|
||||
be used instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
@ -92,6 +104,7 @@ Fixed
|
||||
- `:undo` now doesn't undo tabs "closed" by `:tab-detach` anymore.
|
||||
- Fixed an issue with hint chars not being cleared correctly when leaving hint
|
||||
mode.
|
||||
- `:tab-detach` now fails correctly when there's only one tab open.
|
||||
|
||||
v0.8.3 (unreleased)
|
||||
-------------------
|
||||
@ -102,6 +115,7 @@ Fixed
|
||||
- Fixed crash when doing `:<space><enter>`, another corner-case introduced in v0.8.0
|
||||
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows
|
||||
- Fixed crash when setting `general -> auto-save-interval` to a too big value.
|
||||
- Fixed crash when using hints on Void Linux.
|
||||
|
||||
v0.8.2
|
||||
------
|
||||
@ -111,7 +125,6 @@ Fixed
|
||||
|
||||
- Fixed `general -> private-browsing` not being set correctly until a restart
|
||||
(which caused e.g. local storage to be enabled).
|
||||
- Fixed crash when using hints with JS disabled in some rare circumstances.
|
||||
- When hinting input fields (`:t`), also consider input elements without a type.
|
||||
- Fixed crash when opening an invalid URL with a percent-encoded and a real @ in it
|
||||
- Fixed default `;o` and `;O` bindings
|
||||
|
@ -142,11 +142,11 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Florian Bruhin
|
||||
* Daniel Schadt
|
||||
* Ryan Roden-Corrent
|
||||
* Jakub Klinkovský
|
||||
* Antoni Boucher
|
||||
* Lamar Pavel
|
||||
* Bruno Oliveira
|
||||
* Jakub Klinkovský
|
||||
* Jan Verbeek
|
||||
* Bruno Oliveira
|
||||
* Alexander Cogneau
|
||||
* Marshall Lochbaum
|
||||
* Felix Van der Jeugt
|
||||
@ -169,19 +169,21 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* avk
|
||||
* ZDarian
|
||||
* Niklas Haas
|
||||
* Milan Svoboda
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Peter Vilim
|
||||
* Clayton Craft
|
||||
* nanjekyejoannah
|
||||
* Oliver Caldwell
|
||||
* Niklas Haas
|
||||
* Jonas Schürmann
|
||||
* error800
|
||||
* Michael Hoang
|
||||
* Liam BEGUIN
|
||||
* skinnay
|
||||
* Zach-Button
|
||||
* Tomasz Kramkowski
|
||||
* Peter Rice
|
||||
* Ismail S
|
||||
* Halfwit
|
||||
* David Vogt
|
||||
@ -192,7 +194,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Nick Ginther
|
||||
* Michał Góral
|
||||
* Michael Ilsaas
|
||||
* Michael Hoang
|
||||
* Martin Zimmermann
|
||||
* Fritz Reichwald
|
||||
* Brian Jackson
|
||||
|
@ -4,6 +4,18 @@
|
||||
|
||||
= Commands
|
||||
|
||||
In qutebrowser, all keybindings are mapped to commands.
|
||||
|
||||
Some commands are hidden, which means they don't show up in the command
|
||||
completion when pressing `:`, as they're typically not useful to run by hand.
|
||||
|
||||
In the commandline, there are also some variables you can use:
|
||||
|
||||
- `{url}` expands to the URL of the current page
|
||||
- `{url:pretty}` expands to the URL in decoded format
|
||||
- `{clipboard}` expands to the clipboard contents
|
||||
- `{primary}` expands to the primary selection contents
|
||||
|
||||
== Normal commands
|
||||
.Quick reference
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
@ -32,6 +44,7 @@
|
||||
|<<hint,hint>>|Start hinting.
|
||||
|<<history-clear,history-clear>>|Clear all browsing history.
|
||||
|<<home,home>>|Open main startpage in current tab.
|
||||
|<<insert-text,insert-text>>|Insert text at cursor position.
|
||||
|<<inspector,inspector>>|Toggle the web inspector.
|
||||
|<<jseval,jseval>>|Evaluate a JavaScript string.
|
||||
|<<later,later>>|Execute a command after some time.
|
||||
@ -68,6 +81,7 @@
|
||||
|<<unbind,unbind>>|Unbind a keychain.
|
||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|<<view-source,view-source>>|Show the source of the current page.
|
||||
|<<window-only,window-only>>|Close all windows except for the current one.
|
||||
|<<wq,wq>>|Save open pages and quit.
|
||||
|<<yank,yank>>|Yank something to the clipboard or primary selection.
|
||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||
@ -402,6 +416,18 @@ Note this only clears the global history (e.g. `~/.local/share/qutebrowser/histo
|
||||
=== home
|
||||
Open main startpage in current tab.
|
||||
|
||||
[[insert-text]]
|
||||
=== insert-text
|
||||
Syntax: +:insert-text 'text'+
|
||||
|
||||
Insert text at cursor position.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to insert.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
|
||||
[[inspector]]
|
||||
=== inspector
|
||||
Toggle the web inspector.
|
||||
@ -480,6 +506,10 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
|
||||
* +*-b*+, +*--bg*+: Open in a background tab.
|
||||
* +*-w*+, +*--window*+: Open in a new window.
|
||||
|
||||
==== count
|
||||
For `increment` and `decrement`, the number to change the URL by. For `up`, the number of levels to go up in the URL.
|
||||
|
||||
|
||||
[[open]]
|
||||
=== open
|
||||
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+
|
||||
@ -844,6 +874,10 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
=== view-source
|
||||
Show the source of the current page.
|
||||
|
||||
[[window-only]]
|
||||
=== window-only
|
||||
Close all windows except for the current one.
|
||||
|
||||
[[wq]]
|
||||
=== wq
|
||||
Syntax: +:wq ['name']+
|
||||
@ -940,7 +974,6 @@ How many steps to zoom out.
|
||||
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|
||||
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|
||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||
|<<paste-primary,paste-primary>>|Paste the primary selection at cursor position.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|
||||
@ -997,7 +1030,7 @@ Syntax: +:completion-item-focus 'which'+
|
||||
Shift the focus of the completion menu to another item.
|
||||
|
||||
==== positional arguments
|
||||
* +'which'+: 'next' or 'prev'
|
||||
* +'which'+: 'next', 'prev', 'next-category', or 'prev-category'.
|
||||
|
||||
[[drop-selection]]
|
||||
=== drop-selection
|
||||
@ -1169,10 +1202,6 @@ Open an external editor with the currently selected form field.
|
||||
|
||||
The editor which should be launched can be configured via the `general -> editor` config option.
|
||||
|
||||
[[paste-primary]]
|
||||
=== paste-primary
|
||||
Paste the primary selection at cursor position.
|
||||
|
||||
[[prompt-accept]]
|
||||
=== prompt-accept
|
||||
Accept the current prompt.
|
||||
|
@ -185,7 +185,7 @@
|
||||
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored for number hints.
|
||||
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|
||||
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|
||||
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|
||||
|<<hints-auto-follow,auto-follow>>|Controls when a hint can be automatically followed without the user pressing Enter.
|
||||
|<<hints-auto-follow-timeout,auto-follow-timeout>>|A timeout (in milliseconds) to inhibit normal-mode key bindings after a successful auto-follow.
|
||||
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|
||||
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|
||||
@ -1660,14 +1660,16 @@ Default: +pass:[/usr/share/dict/words]+
|
||||
|
||||
[[hints-auto-follow]]
|
||||
=== auto-follow
|
||||
Follow a hint immediately when the hint text is completely matched.
|
||||
Controls when a hint can be automatically followed without the user pressing Enter.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +always+: Auto-follow whenever there is only a single hint on a page.
|
||||
* +unique-match+: Auto-follow whenever there is a unique non-empty match in either the hint string (word mode) or filter (number mode).
|
||||
* +full-match+: Follow the hint when the user typed the whole hint (letter, word or number mode) or the element's text (only in number mode).
|
||||
* +never+: The user will always need to press Enter to follow a hint.
|
||||
|
||||
Default: +pass:[true]+
|
||||
Default: +pass:[unique-match]+
|
||||
|
||||
[[hints-auto-follow-timeout]]
|
||||
=== auto-follow-timeout
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.31
|
||||
check-manifest==0.32
|
||||
|
@ -42,4 +42,8 @@ git+https://github.com/pallets/jinja.git
|
||||
git+https://github.com/pallets/markupsafe.git
|
||||
hg+http://bitbucket.org/birkenfeld/pygments-main
|
||||
hg+https://bitbucket.org/fdik/pypeg
|
||||
hg+https://bitbucket.org/xi/pyyaml
|
||||
|
||||
# Fails to build:
|
||||
# gcc: error: ext/_yaml.c: No such file or directory
|
||||
# hg+https://bitbucket.org/xi/pyyaml
|
||||
PyYAML==3.11
|
||||
|
@ -424,6 +424,8 @@ class CommandDispatcher:
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def tab_detach(self):
|
||||
"""Detach the current tab to its own window."""
|
||||
if self._count() < 2:
|
||||
raise cmdexc.CommandError("Cannot detach one tab.")
|
||||
url = self._current_url()
|
||||
self._open(url, window=True)
|
||||
cur_widget = self._current_widget()
|
||||
@ -482,7 +484,8 @@ class CommandDispatcher:
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
|
||||
'decrement'])
|
||||
def navigate(self, where: str, tab=False, bg=False, window=False):
|
||||
@cmdutils.argument('count', count=True)
|
||||
def navigate(self, where: str, tab=False, bg=False, window=False, count=1):
|
||||
"""Open typical prev/next links or navigate using the URL path.
|
||||
|
||||
This tries to automatically click on typical _Previous Page_ or
|
||||
@ -502,6 +505,8 @@ class CommandDispatcher:
|
||||
tab: Open in a new tab.
|
||||
bg: Open in a background tab.
|
||||
window: Open in a new window.
|
||||
count: For `increment` and `decrement`, the number to change the
|
||||
URL by. For `up`, the number of levels to go up in the URL.
|
||||
"""
|
||||
# save the pre-jump position in the special ' mark
|
||||
self.set_mark("'")
|
||||
@ -526,7 +531,7 @@ class CommandDispatcher:
|
||||
handler(browsertab=widget, win_id=self._win_id, baseurl=url,
|
||||
tab=tab, background=bg, window=window)
|
||||
elif where in ['up', 'increment', 'decrement']:
|
||||
new_url = handlers[where](url)
|
||||
new_url = handlers[where](url, count)
|
||||
self._open(new_url, tab, bg, window)
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Got called with invalid value {} for "
|
||||
@ -1486,10 +1491,25 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError("Element vanished while editing!")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher',
|
||||
deprecated="Use :insert-text {primary}",
|
||||
modes=[KeyMode.insert], hide=True, scope='window',
|
||||
needs_js=True, backend=usertypes.Backend.QtWebKit)
|
||||
def paste_primary(self):
|
||||
"""Paste the primary selection at cursor position."""
|
||||
try:
|
||||
self.insert_text(utils.get_clipboard(selection=True))
|
||||
except utils.SelectionUnsupportedError:
|
||||
self.insert_text(utils.get_clipboard())
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||
scope='window', needs_js=True,
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
def insert_text(self, text):
|
||||
"""Insert text at cursor position.
|
||||
|
||||
Args:
|
||||
text: The text to insert.
|
||||
"""
|
||||
# FIXME:qtwebengine have a proper API for this
|
||||
tab = self._current_widget()
|
||||
page = tab._widget.page() # pylint: disable=protected-access
|
||||
@ -1499,23 +1519,14 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError("No element focused!")
|
||||
if not elem.is_editable(strict=True):
|
||||
raise cmdexc.CommandError("Focused element is not editable!")
|
||||
|
||||
try:
|
||||
try:
|
||||
sel = utils.get_clipboard(selection=True)
|
||||
except utils.SelectionUnsupportedError:
|
||||
sel = utils.get_clipboard()
|
||||
except utils.ClipboardEmptyError:
|
||||
return
|
||||
|
||||
log.misc.debug("Pasting primary selection into element {}".format(
|
||||
log.misc.debug("Inserting text into element {}".format(
|
||||
elem.debug_text()))
|
||||
elem.run_js_async("""
|
||||
var sel = '{}';
|
||||
var text = '{}';
|
||||
var event = document.createEvent('TextEvent');
|
||||
event.initTextEvent('textInput', true, true, null, sel);
|
||||
event.initTextEvent('textInput', true, true, null, text);
|
||||
this.dispatchEvent(event);
|
||||
""".format(javascript.string_escape(sel)))
|
||||
""".format(javascript.string_escape(text)))
|
||||
|
||||
def _search_cb(self, found, *, tab, old_scroll_pos, options, text, prev):
|
||||
"""Callback called from search/search_next/search_prev.
|
||||
|
@ -62,7 +62,6 @@ class HintContext:
|
||||
"""Context namespace used for hinting.
|
||||
|
||||
Attributes:
|
||||
frames: The QWebFrames to use.
|
||||
all_elems: A list of all (elem, label) namedtuples ever created.
|
||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||
May contain less elements than `all_elems` due to filtering.
|
||||
@ -79,6 +78,7 @@ class HintContext:
|
||||
to_follow: The link to follow when enter is pressed.
|
||||
args: Custom arguments for userscript/spawn
|
||||
rapid: Whether to do rapid hinting.
|
||||
filterstr: Used to save the filter string for restoring in rapid mode.
|
||||
tab: The WebTab object we started hinting in.
|
||||
group: The group of web elements to hint.
|
||||
"""
|
||||
@ -90,7 +90,7 @@ class HintContext:
|
||||
self.baseurl = None
|
||||
self.to_follow = None
|
||||
self.rapid = False
|
||||
self.frames = []
|
||||
self.filterstr = None
|
||||
self.args = []
|
||||
self.tab = None
|
||||
self.group = None
|
||||
@ -309,7 +309,6 @@ class HintManager(QObject):
|
||||
_context: The HintContext for the current invocation.
|
||||
_win_id: The window ID this HintManager is associated with.
|
||||
_tab_id: The tab ID this HintManager is associated with.
|
||||
_filterstr: Used to save the filter string for restoring in rapid mode.
|
||||
|
||||
Signals:
|
||||
See HintActions
|
||||
@ -342,7 +341,6 @@ class HintManager(QObject):
|
||||
self._win_id = win_id
|
||||
self._tab_id = tab_id
|
||||
self._context = None
|
||||
self._filterstr = None
|
||||
self._word_hinter = WordHinter()
|
||||
|
||||
self._actions = HintActions(win_id)
|
||||
@ -381,7 +379,6 @@ class HintManager(QObject):
|
||||
window=self._win_id)
|
||||
message_bridge.maybe_reset_text(text)
|
||||
self._context = None
|
||||
self._filterstr = None
|
||||
|
||||
def _hint_strings(self, elems):
|
||||
"""Calculate the hint strings for elems.
|
||||
@ -394,6 +391,8 @@ class HintManager(QObject):
|
||||
Return:
|
||||
A list of hint strings, in the same order as the elements.
|
||||
"""
|
||||
if not elems:
|
||||
return []
|
||||
hint_mode = self._context.hint_mode
|
||||
if hint_mode == 'word':
|
||||
try:
|
||||
@ -630,6 +629,15 @@ class HintManager(QObject):
|
||||
# Do multi-word matching
|
||||
return all(word in elemstr for word in filterstr.split())
|
||||
|
||||
def _filter_matches_exactly(self, filterstr, elemstr):
|
||||
"""Return True if `filterstr` exactly matches `elemstr`."""
|
||||
# Empty string and None never match
|
||||
if not filterstr:
|
||||
return False
|
||||
filterstr = filterstr.casefold()
|
||||
elemstr = elemstr.casefold()
|
||||
return filterstr == elemstr
|
||||
|
||||
def _start_cb(self, elems):
|
||||
"""Initialize the elements and labels based on the context set."""
|
||||
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
|
||||
@ -656,6 +664,9 @@ class HintManager(QObject):
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.hint,
|
||||
'HintManager.start')
|
||||
|
||||
# to make auto-follow == 'always' work
|
||||
self._handle_auto_follow()
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
|
||||
star_args_optional=True, maxsplit=2,
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
@ -753,7 +764,6 @@ class HintManager(QObject):
|
||||
self._context.baseurl = tabbed_browser.current_url()
|
||||
except qtutils.QtValueError:
|
||||
raise cmdexc.CommandError("No URL set for this page yet!")
|
||||
self._context.tab = tab
|
||||
self._context.args = args
|
||||
self._context.group = group
|
||||
selector = webelem.SELECTORS[self._context.group]
|
||||
@ -767,6 +777,51 @@ class HintManager(QObject):
|
||||
|
||||
return self._context.hint_mode
|
||||
|
||||
def _get_visible_hints(self):
|
||||
"""Get elements which are currently visible."""
|
||||
visible = {}
|
||||
for string, elem in self._context.elems.items():
|
||||
try:
|
||||
if not self._is_hidden(elem.label):
|
||||
visible[string] = elem
|
||||
except webelem.Error:
|
||||
pass
|
||||
return visible
|
||||
|
||||
def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
|
||||
"""Handle the auto-follow option."""
|
||||
if visible is None:
|
||||
visible = self._get_visible_hints()
|
||||
|
||||
if len(visible) != 1:
|
||||
return
|
||||
|
||||
auto_follow = config.get('hints', 'auto-follow')
|
||||
|
||||
if auto_follow == "always":
|
||||
follow = True
|
||||
elif auto_follow == "unique-match":
|
||||
follow = keystr or filterstr
|
||||
elif auto_follow == "full-match":
|
||||
elemstr = str(list(visible.values())[0].elem)
|
||||
filter_match = self._filter_matches_exactly(filterstr, elemstr)
|
||||
follow = (keystr in visible) or filter_match
|
||||
else:
|
||||
follow = False
|
||||
# save the keystr of the only one visible hint to be picked up
|
||||
# later by self.follow_hint
|
||||
self._context.to_follow = list(visible.keys())[0]
|
||||
|
||||
if follow:
|
||||
# apply auto-follow-timeout
|
||||
timeout = config.get('hints', 'auto-follow-timeout')
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
normal_parser = keyparsers[usertypes.KeyMode.normal]
|
||||
normal_parser.set_inhibited_timeout(timeout)
|
||||
# unpacking gets us the first (and only) key in the dict.
|
||||
self._fire(*visible)
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||
@ -790,57 +845,7 @@ class HintManager(QObject):
|
||||
self._hide_elem(elem.label)
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
def _filter_number_hints(self):
|
||||
"""Apply filters for numbered hints and renumber them.
|
||||
|
||||
Return:
|
||||
Elements which are still visible
|
||||
"""
|
||||
# renumber filtered hints
|
||||
elems = []
|
||||
for e in self._context.all_elems:
|
||||
try:
|
||||
if not self._is_hidden(e.label):
|
||||
elems.append(e)
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not elems:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'all filtered')
|
||||
return {}
|
||||
|
||||
strings = self._hint_strings(elems)
|
||||
self._context.elems = {}
|
||||
for elem, string in zip(elems, strings):
|
||||
elem.label.set_inner_xml(string)
|
||||
self._context.elems[string] = elem
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
keyparser.update_bindings(strings, preserve_filter=True)
|
||||
|
||||
return self._context.elems
|
||||
|
||||
def _filter_non_number_hints(self):
|
||||
"""Apply filters for letter/word hints.
|
||||
|
||||
Return:
|
||||
Elements which are still visible
|
||||
"""
|
||||
visible = {}
|
||||
for string, elem in self._context.elems.items():
|
||||
try:
|
||||
if not self._is_hidden(elem.label):
|
||||
visible[string] = elem
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'all filtered')
|
||||
return visible
|
||||
self._handle_auto_follow(keystr=keystr)
|
||||
|
||||
def filter_hints(self, filterstr):
|
||||
"""Filter displayed hints according to a text.
|
||||
@ -852,13 +857,15 @@ class HintManager(QObject):
|
||||
and `self._filterstr` are None, all hints are shown.
|
||||
"""
|
||||
if filterstr is None:
|
||||
filterstr = self._filterstr
|
||||
filterstr = self._context.filterstr
|
||||
else:
|
||||
self._filterstr = filterstr
|
||||
self._context.filterstr = filterstr
|
||||
|
||||
visible = []
|
||||
for elem in self._context.all_elems:
|
||||
try:
|
||||
if self._filter_matches(filterstr, str(elem.elem)):
|
||||
visible.append(elem)
|
||||
if self._is_hidden(elem.label):
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elem.label)
|
||||
@ -868,35 +875,37 @@ class HintManager(QObject):
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
if self._context.hint_mode == 'number':
|
||||
visible = self._filter_number_hints()
|
||||
else:
|
||||
visible = self._filter_non_number_hints()
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'all filtered')
|
||||
return
|
||||
|
||||
if (len(visible) == 1 and
|
||||
config.get('hints', 'auto-follow') and
|
||||
filterstr is not None):
|
||||
# apply auto-follow-timeout
|
||||
timeout = config.get('hints', 'auto-follow-timeout')
|
||||
if self._context.hint_mode == 'number':
|
||||
# renumber filtered hints
|
||||
strings = self._hint_strings(visible)
|
||||
self._context.elems = {}
|
||||
for elem, string in zip(visible, strings):
|
||||
elem.label.set_inner_xml(string)
|
||||
self._context.elems[string] = elem
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
normal_parser = keyparsers[usertypes.KeyMode.normal]
|
||||
normal_parser.set_inhibited_timeout(timeout)
|
||||
# unpacking gets us the first (and only) key in the dict.
|
||||
self.fire(*visible)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
keyparser.update_bindings(strings, preserve_filter=True)
|
||||
|
||||
def fire(self, keystr, force=False):
|
||||
# Note: filter_hints can be called with non-None filterstr only
|
||||
# when number mode is active
|
||||
if filterstr is not None:
|
||||
# pass self._context.elems as the dict of visible hints
|
||||
self._handle_auto_follow(filterstr=filterstr,
|
||||
visible=self._context.elems)
|
||||
|
||||
def _fire(self, keystr):
|
||||
"""Fire a completed hint.
|
||||
|
||||
Args:
|
||||
keystr: The keychain string to follow.
|
||||
force: When True, follow even when auto-follow is false.
|
||||
"""
|
||||
if not (force or config.get('hints', 'auto-follow')):
|
||||
self.handle_partial_key(keystr)
|
||||
self._context.to_follow = keystr
|
||||
return
|
||||
|
||||
# Handlers which take a QWebElement
|
||||
elem_handlers = {
|
||||
Target.normal: self._actions.click,
|
||||
@ -969,7 +978,7 @@ class HintManager(QObject):
|
||||
keystring = self._context.to_follow
|
||||
elif keystring not in self._context.elems:
|
||||
raise cmdexc.CommandError("No hint {}!".format(keystring))
|
||||
self.fire(keystring, force=True)
|
||||
self._fire(keystring)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_contents_size_changed(self):
|
||||
|
@ -31,11 +31,12 @@ class Error(Exception):
|
||||
"""Raised when the navigation can't be done."""
|
||||
|
||||
|
||||
def incdec(url, inc_or_dec):
|
||||
def incdec(url, count, inc_or_dec):
|
||||
"""Helper method for :navigate when `where' is increment/decrement.
|
||||
|
||||
Args:
|
||||
url: The current url.
|
||||
count: How much to increment or decrement by.
|
||||
inc_or_dec: Either 'increment' or 'decrement'.
|
||||
tab: Whether to open the link in a new tab.
|
||||
background: Open the link in a new background tab.
|
||||
@ -43,23 +44,26 @@ def incdec(url, inc_or_dec):
|
||||
"""
|
||||
segments = set(config.get('general', 'url-incdec-segments'))
|
||||
try:
|
||||
new_url = urlutils.incdec_number(url, inc_or_dec, segments=segments)
|
||||
new_url = urlutils.incdec_number(url, inc_or_dec, count,
|
||||
segments=segments)
|
||||
except urlutils.IncDecError as error:
|
||||
raise Error(error.msg)
|
||||
return new_url
|
||||
|
||||
|
||||
def path_up(url):
|
||||
def path_up(url, count):
|
||||
"""Helper method for :navigate when `where' is up.
|
||||
|
||||
Args:
|
||||
url: The current url.
|
||||
count: The number of levels to go up in the url.
|
||||
"""
|
||||
path = url.path()
|
||||
if not path or path == '/':
|
||||
raise Error("Can't go up!")
|
||||
new_path = posixpath.join(path, posixpath.pardir)
|
||||
url.setPath(new_path)
|
||||
for _i in range(0, min(count, path.count('/'))):
|
||||
path = posixpath.join(path, posixpath.pardir)
|
||||
url.setPath(path)
|
||||
return url
|
||||
|
||||
|
||||
|
@ -17,4 +17,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Utilities and classes regarding to commands."""
|
||||
"""In qutebrowser, all keybindings are mapped to commands.
|
||||
|
||||
Some commands are hidden, which means they don't show up in the command
|
||||
completion when pressing `:`, as they're typically not useful to run by hand.
|
||||
|
||||
In the commandline, there are also some variables you can use:
|
||||
|
||||
- `{url}` expands to the URL of the current page
|
||||
- `{url:pretty}` expands to the URL in decoded format
|
||||
- `{clipboard}` expands to the clipboard contents
|
||||
- `{primary}` expands to the primary selection contents
|
||||
"""
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
import collections
|
||||
import traceback
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
|
||||
|
||||
@ -50,26 +51,32 @@ def _current_url(tabbed_browser):
|
||||
def replace_variables(win_id, arglist):
|
||||
"""Utility function to replace variables like {url} in a list of args."""
|
||||
variables = {
|
||||
'{url}': lambda: _current_url(tabbed_browser).toString(
|
||||
'url': lambda: _current_url(tabbed_browser).toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword),
|
||||
'{url:pretty}': lambda: _current_url(tabbed_browser).toString(
|
||||
'url:pretty': lambda: _current_url(tabbed_browser).toString(
|
||||
QUrl.RemovePassword),
|
||||
'{clipboard}': utils.get_clipboard,
|
||||
'{primary}': lambda: utils.get_clipboard(selection=True),
|
||||
'clipboard': utils.get_clipboard,
|
||||
'primary': lambda: utils.get_clipboard(selection=True),
|
||||
}
|
||||
values = {}
|
||||
args = []
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
|
||||
def repl_cb(matchobj):
|
||||
"""Return replacement for given match."""
|
||||
var = matchobj.group("var")
|
||||
if var not in values:
|
||||
values[var] = variables[var]()
|
||||
return values[var]
|
||||
repl_pattern = re.compile("{(?P<var>" + "|".join(variables.keys()) + ")}")
|
||||
|
||||
try:
|
||||
for arg in arglist:
|
||||
for var, func in variables.items():
|
||||
if var in arg:
|
||||
if var not in values:
|
||||
values[var] = func()
|
||||
arg = arg.replace(var, values[var])
|
||||
args.append(arg)
|
||||
# using re.sub with callback function replaces all variables in a
|
||||
# single pass and avoids expansion of nested variables (e.g.
|
||||
# "{url}" from clipboard is not expanded)
|
||||
args.append(repl_pattern.sub(repl_cb, arg))
|
||||
except utils.ClipboardError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
return args
|
||||
|
@ -181,14 +181,44 @@ class CompletionView(QTreeView):
|
||||
# Item is a real item, not a category header -> success
|
||||
return idx
|
||||
|
||||
def _next_category_idx(self, upwards):
|
||||
"""Get the index of the previous/next category.
|
||||
|
||||
Args:
|
||||
upwards: Get previous item, not next.
|
||||
|
||||
Return:
|
||||
A QModelIndex.
|
||||
"""
|
||||
idx = self.selectionModel().currentIndex()
|
||||
if not idx.isValid():
|
||||
return self._next_idx(upwards).sibling(0, 0)
|
||||
idx = idx.parent()
|
||||
direction = -1 if upwards else 1
|
||||
while True:
|
||||
idx = idx.sibling(idx.row() + direction, 0)
|
||||
if not idx.isValid() and upwards:
|
||||
# wrap around to the first item of the last category
|
||||
return self.model().last_item().sibling(0, 0)
|
||||
elif not idx.isValid() and not upwards:
|
||||
# wrap around to the first item of the first category
|
||||
idx = self.model().first_item()
|
||||
self.scrollTo(idx.parent())
|
||||
return idx
|
||||
elif idx.isValid() and idx.child(0, 0).isValid():
|
||||
# scroll to ensure the category is visible
|
||||
self.scrollTo(idx)
|
||||
return idx.child(0, 0)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
@cmdutils.argument('which', choices=['next', 'prev'])
|
||||
@cmdutils.argument('which', choices=['next', 'prev', 'next-category',
|
||||
'prev-category'])
|
||||
def completion_item_focus(self, which):
|
||||
"""Shift the focus of the completion menu to another item.
|
||||
|
||||
Args:
|
||||
which: 'next' or 'prev'
|
||||
which: 'next', 'prev', 'next-category', or 'prev-category'.
|
||||
"""
|
||||
# selmodel can be None if 'show' and 'auto-open' are set to False
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1731
|
||||
@ -196,7 +226,15 @@ class CompletionView(QTreeView):
|
||||
if selmodel is None:
|
||||
return
|
||||
|
||||
idx = self._next_idx(which == 'prev')
|
||||
if which == 'next':
|
||||
idx = self._next_idx(upwards=False)
|
||||
elif which == 'prev':
|
||||
idx = self._next_idx(upwards=True)
|
||||
elif which == 'next-category':
|
||||
idx = self._next_category_idx(upwards=False)
|
||||
elif which == 'prev-category':
|
||||
idx = self._next_category_idx(upwards=True)
|
||||
|
||||
if not idx.isValid():
|
||||
return
|
||||
|
||||
|
@ -365,6 +365,8 @@ class ConfigManager(QObject):
|
||||
_get_value_transformer({'false': 'none', 'true': 'debug'}),
|
||||
('ui', 'keyhint-blacklist'):
|
||||
_get_value_transformer({'false': '*', 'true': ''}),
|
||||
('hints', 'auto-follow'):
|
||||
_get_value_transformer({'false': 'never', 'true': 'unique-match'}),
|
||||
}
|
||||
|
||||
changed = pyqtSignal(str, str)
|
||||
|
@ -936,9 +936,21 @@ def data(readonly=False):
|
||||
"The dictionary file to be used by the word hints."),
|
||||
|
||||
('auto-follow',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Follow a hint immediately when the hint text is completely "
|
||||
"matched."),
|
||||
SettingValue(typ.String(
|
||||
valid_values=typ.ValidValues(
|
||||
('always', "Auto-follow whenever there is only a single "
|
||||
"hint on a page."),
|
||||
('unique-match', "Auto-follow whenever there is a unique "
|
||||
"non-empty match in either the hint string (word mode) "
|
||||
"or filter (number mode)."),
|
||||
('full-match', "Follow the hint when the user typed the "
|
||||
"whole hint (letter, word or number mode) or the "
|
||||
"element's text (only in number mode)."),
|
||||
('never', "The user will always need to press Enter to "
|
||||
"follow a hint."),
|
||||
)), 'unique-match'),
|
||||
"Controls when a hint can be automatically followed without the "
|
||||
"user pressing Enter."),
|
||||
|
||||
('auto-follow-timeout',
|
||||
SettingValue(typ.Int(), '0'),
|
||||
@ -1586,7 +1598,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
|
||||
('insert', collections.OrderedDict([
|
||||
('open-editor', ['<Ctrl-E>']),
|
||||
('paste-primary', ['<Shift-Ins>']),
|
||||
('insert-text {primary}', ['<Shift-Ins>']),
|
||||
])),
|
||||
|
||||
('hint', collections.OrderedDict([
|
||||
@ -1603,6 +1615,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('command-history-next', ['<Ctrl-N>']),
|
||||
('completion-item-focus prev', ['<Shift-Tab>', '<Up>']),
|
||||
('completion-item-focus next', ['<Tab>', '<Down>']),
|
||||
('completion-item-focus next-category', ['<Ctrl-Tab>']),
|
||||
('completion-item-focus prev-category', ['<Ctrl-Shift-Tab>']),
|
||||
('completion-item-del', ['<Ctrl-D>']),
|
||||
('command-accept', RETURN_KEYS),
|
||||
])),
|
||||
@ -1711,4 +1725,6 @@ CHANGED_KEY_COMMANDS = [
|
||||
(re.compile(r'^open -([twb]) {clipboard}$'), r'open -\1 -- {clipboard}'),
|
||||
(re.compile(r'^open {primary}$'), r'open -- {primary}'),
|
||||
(re.compile(r'^open -([twb]) {primary}$'), r'open -\1 -- {primary}'),
|
||||
|
||||
(re.compile(r'^paste-primary$'), r'insert-text {primary}'),
|
||||
]
|
||||
|
@ -231,7 +231,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
if keytype == self.Type.chain:
|
||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||
window=self._win_id, tab='current')
|
||||
hintmanager.fire(cmdstr)
|
||||
hintmanager.handle_partial_key(cmdstr)
|
||||
else:
|
||||
# execute as command
|
||||
super().execute(cmdstr, keytype, count)
|
||||
|
@ -66,6 +66,9 @@ class TabWidget(QTabWidget):
|
||||
@config.change_filter('tabs')
|
||||
def init_config(self):
|
||||
"""Initialize attributes based on the config."""
|
||||
if self is None: # pragma: no cover
|
||||
# WORKAROUND for PyQt 5.2
|
||||
return
|
||||
tabbar = self.tabBar()
|
||||
self.setMovable(config.get('tabs', 'movable'))
|
||||
self.setTabsClosable(False)
|
||||
|
@ -247,3 +247,12 @@ def log_capacity(capacity: int):
|
||||
raise cmdexc.CommandError("Can't set a negative log capacity!")
|
||||
else:
|
||||
log.ram_handler.change_log_capacity(capacity)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('current_win_id', win_id=True)
|
||||
def window_only(current_win_id):
|
||||
"""Close all windows except for the current one."""
|
||||
for win_id, window in objreg.window_registry.items():
|
||||
if win_id != current_win_id:
|
||||
window.close()
|
||||
|
@ -499,7 +499,7 @@ class IncDecError(Exception):
|
||||
return '{}: {}'.format(self.msg, self.url.toString())
|
||||
|
||||
|
||||
def _get_incdec_value(match, incdec, url):
|
||||
def _get_incdec_value(match, incdec, url, count):
|
||||
"""Get an incremented/decremented URL based on a URL match."""
|
||||
pre, zeroes, number, post = match.groups()
|
||||
# This should always succeed because we match \d+
|
||||
@ -507,9 +507,9 @@ def _get_incdec_value(match, incdec, url):
|
||||
if incdec == 'decrement':
|
||||
if val <= 0:
|
||||
raise IncDecError("Can't decrement {}!".format(val), url)
|
||||
val -= 1
|
||||
val -= count
|
||||
elif incdec == 'increment':
|
||||
val += 1
|
||||
val += count
|
||||
else:
|
||||
raise ValueError("Invalid value {} for indec!".format(incdec))
|
||||
if zeroes:
|
||||
@ -521,12 +521,13 @@ def _get_incdec_value(match, incdec, url):
|
||||
return ''.join([pre, zeroes, str(val), post])
|
||||
|
||||
|
||||
def incdec_number(url, incdec, segments=None):
|
||||
def incdec_number(url, incdec, count=1, segments=None):
|
||||
"""Find a number in the url and increment or decrement it.
|
||||
|
||||
Args:
|
||||
url: The current url
|
||||
incdec: Either 'increment' or 'decrement'
|
||||
count: The number to increment or decrement by
|
||||
segments: A set of URL segments to search. Valid segments are:
|
||||
'host', 'path', 'query', 'anchor'.
|
||||
Default: {'path', 'query'}
|
||||
@ -566,7 +567,7 @@ def incdec_number(url, incdec, segments=None):
|
||||
if not match:
|
||||
continue
|
||||
|
||||
setter(_get_incdec_value(match, incdec, url))
|
||||
setter(_get_incdec_value(match, incdec, url, count))
|
||||
return url
|
||||
|
||||
raise IncDecError("No number found in URL!", url)
|
||||
|
@ -37,7 +37,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
|
||||
# We import qutebrowser.app so all @cmdutils-register decorators are run.
|
||||
import qutebrowser.app
|
||||
from scripts import asciidoc2html, utils
|
||||
from qutebrowser import qutebrowser
|
||||
from qutebrowser import qutebrowser, commands
|
||||
from qutebrowser.commands import cmdutils, argparser
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.utils import docutils, usertypes
|
||||
@ -320,7 +320,8 @@ def generate_commands(filename):
|
||||
"""Generate the complete commands section."""
|
||||
with _open_file(filename) as f:
|
||||
f.write(FILE_HEADER)
|
||||
f.write("= Commands\n")
|
||||
f.write("= Commands\n\n")
|
||||
f.write(commands.__doc__)
|
||||
normal_cmds = []
|
||||
hidden_cmds = []
|
||||
debug_cmds = []
|
||||
|
@ -6,8 +6,19 @@
|
||||
<title>Many links</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/data/hello.txt">1</a>
|
||||
<a href="/data/hello2.txt">2</a>
|
||||
<a href="/data/hello3.txt">3</a>
|
||||
<a href="/data/numbers/1.txt">1</a>
|
||||
<a href="/data/numbers/2.txt">2</a>
|
||||
<a href="/data/numbers/3.txt">3</a>
|
||||
<a href="/data/numbers/4.txt">4</a>
|
||||
<a href="/data/numbers/5.txt">5</a>
|
||||
<a href="/data/numbers/6.txt">6</a>
|
||||
<a href="/data/numbers/7.txt">7</a>
|
||||
<a href="/data/numbers/8.txt">8</a>
|
||||
<a href="/data/numbers/9.txt">9</a>
|
||||
<a href="/data/numbers/10.txt">10</a>
|
||||
<a href="/data/numbers/11.txt">11</a>
|
||||
<a href="/data/numbers/12.txt">12</a>
|
||||
<a href="/data/numbers/13.txt">13</a>
|
||||
<a href="/data/numbers/14.txt">14</a>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -108,7 +108,7 @@ Feature: Going back and forward.
|
||||
And the message "Still alive!" should be shown
|
||||
|
||||
Scenario: Going back in a new window
|
||||
Given I have a fresh instance
|
||||
Given I clean up open tabs
|
||||
When I open data/backforward/1.txt
|
||||
And I open data/backforward/2.txt
|
||||
And I run :back -w
|
||||
|
@ -139,6 +139,15 @@ def fresh_instance(quteproc):
|
||||
quteproc.start()
|
||||
|
||||
|
||||
@bdd.given("I clean up open tabs")
|
||||
def clean_open_tabs(quteproc):
|
||||
"""Clean up open windows and tabs."""
|
||||
quteproc.set_setting('tabs', 'last-close', 'blank')
|
||||
quteproc.send_cmd(':window-only')
|
||||
quteproc.send_cmd(':tab-only')
|
||||
quteproc.send_cmd(':tab-close')
|
||||
|
||||
|
||||
## When
|
||||
|
||||
|
||||
|
@ -39,10 +39,10 @@ Feature: Using hints
|
||||
- data/hello.txt (active)
|
||||
|
||||
Scenario: Entering and leaving hinting mode (issue 1464)
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint
|
||||
And I run :fake-key -g <Esc>
|
||||
Then no crash should happen
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint
|
||||
And I run :fake-key -g <Esc>
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: Using :hint spawn with flags and -- (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
@ -188,6 +188,17 @@ Feature: Using hints
|
||||
And I press the key ","
|
||||
Then the message "Keypress worked!" should be shown
|
||||
|
||||
### Word hints
|
||||
|
||||
Scenario: Hinting with a too short dictionary
|
||||
When I open data/hints/short_dict.html
|
||||
And I set hints -> mode to word
|
||||
And I run :hint
|
||||
# Test letter fallback
|
||||
And I run :follow-hint d
|
||||
Then the error "Not enough words in the dictionary." should be shown
|
||||
And data/numbers/5.txt should be loaded
|
||||
|
||||
### Number hint mode
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/308
|
||||
@ -225,7 +236,7 @@ Feature: Using hints
|
||||
Scenario: Multi-word matching
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to true
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I set hints -> auto-follow-timeout to 0
|
||||
And I run :hint all
|
||||
And I press the keys "ten pos"
|
||||
@ -265,3 +276,133 @@ Feature: Using hints
|
||||
And I press the key "s"
|
||||
And I run :follow-hint 1
|
||||
Then data/numbers/7.txt should be loaded
|
||||
|
||||
### auto-follow option
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'always' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to always
|
||||
And I run :hint
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
# unique-match is actually the same as full-match in letter mode
|
||||
Scenario: Using hints -> auto-follow == 'unique-match' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I run :hint
|
||||
And I press the key "a"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'full-match' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to full-match
|
||||
And I run :hint
|
||||
And I press the key "a"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' without Enter in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to never
|
||||
And I run :hint
|
||||
And I press the key "a"
|
||||
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to never
|
||||
And I run :hint
|
||||
And I press the key "a"
|
||||
And I press the key "<Enter>"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'always' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to always
|
||||
And I run :hint
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'unique-match' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I run :hint
|
||||
And I press the key "f"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'full-match' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to full-match
|
||||
And I run :hint
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "follow me!"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' without Enter in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to never
|
||||
And I run :hint
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "follow me!"
|
||||
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to never
|
||||
And I run :hint
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "follow me!"
|
||||
And I press the key "<Enter>"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'always' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to always
|
||||
And I run :hint
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'unique-match' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I run :hint
|
||||
# the link gets "hello" as the hint
|
||||
And I press the key "h"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'full-match' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to full-match
|
||||
And I run :hint
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "hello"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' without Enter in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to never
|
||||
And I run :hint
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "hello"
|
||||
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to never
|
||||
And I run :hint
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "hello"
|
||||
And I press the key "<Enter>"
|
||||
Then data/hello.txt should be loaded
|
||||
|
@ -525,6 +525,20 @@ Feature: Various utility commands.
|
||||
- data/hints/link_blank.html
|
||||
- data/hello.txt (active)
|
||||
|
||||
@no_xvfb
|
||||
Scenario: :window-only
|
||||
Given I run :tab-only
|
||||
And I open data/hello.txt
|
||||
When I open data/hello2.txt in a new tab
|
||||
And I open data/hello3.txt in a new window
|
||||
And I run :window-only
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- active: true
|
||||
history:
|
||||
- url: http://localhost:*/data/hello3.txt
|
||||
|
||||
## Variables
|
||||
|
||||
Scenario: {url} as part of an argument
|
||||
@ -537,3 +551,11 @@ Feature: Various utility commands.
|
||||
And I put "foo" into the clipboard
|
||||
And I run :message-info {clipboard}bar{url}
|
||||
Then the message "foobarhttp://localhost:*/hello.txt" should be shown
|
||||
|
||||
@xfail_norun
|
||||
Scenario: {url} in clipboard should not be expanded
|
||||
When I open data/hello.txt
|
||||
# FIXME: {url} should be escaped, otherwise it is replaced before it enters clipboard
|
||||
And I put "{url}" into the clipboard
|
||||
And I run :message-info {clipboard}bar{url}
|
||||
Then the message "{url}barhttp://localhost:*/hello.txt" should be shown
|
||||
|
@ -11,6 +11,11 @@ Feature: Using :navigate
|
||||
And I run :navigate up
|
||||
Then data/navigate should be loaded
|
||||
|
||||
Scenario: Navigating up by count
|
||||
When I open data/navigate/sub/index.html
|
||||
And I run :navigate up with count 2
|
||||
Then data/navigate should be loaded
|
||||
|
||||
# prev/next
|
||||
|
||||
Scenario: Navigating to previous page
|
||||
@ -60,6 +65,16 @@ Feature: Using :navigate
|
||||
And I run :navigate increment
|
||||
Then the error "No number found in URL!" should be shown
|
||||
|
||||
Scenario: Incrementing number in URL by count
|
||||
When I open data/numbers/3.txt
|
||||
And I run :navigate increment with count 3
|
||||
Then data/numbers/6.txt should be loaded
|
||||
|
||||
Scenario: Decrementing number in URL by count
|
||||
When I open data/numbers/8.txt
|
||||
And I run :navigate decrement with count 5
|
||||
Then data/numbers/3.txt should be loaded
|
||||
|
||||
Scenario: Setting url-incdec-segments
|
||||
When I set general -> url-incdec-segments to anchor
|
||||
And I open data/numbers/1.txt
|
||||
|
@ -263,8 +263,7 @@ Feature: Tab management
|
||||
Then the error "There's no tab with index -1!" should be shown
|
||||
|
||||
Scenario: :tab-focus last with no last focused tab
|
||||
Given I have a fresh instance
|
||||
And I run :tab-focus last
|
||||
When I run :tab-focus last
|
||||
Then the error "No last focused tab!" should be shown
|
||||
|
||||
# tab-prev/tab-next
|
||||
@ -564,7 +563,6 @@ Feature: Tab management
|
||||
- data/hello2.txt
|
||||
|
||||
Scenario: Cloning to new window
|
||||
Given I have a fresh instance
|
||||
When I open data/title.html
|
||||
And I run :tab-clone -w
|
||||
Then the session should look like:
|
||||
@ -583,7 +581,6 @@ Feature: Tab management
|
||||
title: Test title
|
||||
|
||||
Scenario: Cloning with tabs-are-windows = true
|
||||
Given I have a fresh instance
|
||||
When I open data/title.html
|
||||
And I set tabs -> tabs-are-windows to true
|
||||
And I run :tab-clone
|
||||
@ -605,7 +602,6 @@ Feature: Tab management
|
||||
# :tab-detach
|
||||
|
||||
Scenario: Detaching a tab
|
||||
Given I have a fresh instance
|
||||
When I open data/numbers/1.txt
|
||||
And I open data/numbers/2.txt in a new tab
|
||||
And I run :tab-detach
|
||||
@ -620,6 +616,11 @@ Feature: Tab management
|
||||
- history:
|
||||
- url: http://localhost:*/data/numbers/2.txt
|
||||
|
||||
Scenario: Detach tab from window with only one tab
|
||||
When I open data/hello.txt
|
||||
And I run :tab-detach
|
||||
Then the error "Cannot detach one tab." should be shown
|
||||
|
||||
# :undo
|
||||
|
||||
Scenario: Undo without any closed tabs
|
||||
@ -856,7 +857,6 @@ Feature: Tab management
|
||||
# :buffer
|
||||
|
||||
Scenario: :buffer without args
|
||||
Given I have a fresh instance
|
||||
When I run :buffer
|
||||
Then the error "buffer: The following arguments are required: index" should be shown
|
||||
|
||||
@ -909,8 +909,8 @@ Feature: Tab management
|
||||
|
||||
Scenario: :buffer with no matching window index
|
||||
When I open data/title.html
|
||||
And I run :buffer "2/1"
|
||||
Then the error "There's no window with id 2!" should be shown
|
||||
And I run :buffer "99/1"
|
||||
Then the error "There's no window with id 99!" should be shown
|
||||
|
||||
Scenario: :buffer with matching window index
|
||||
Given I have a fresh instance
|
||||
@ -942,7 +942,6 @@ Feature: Tab management
|
||||
- url: http://localhost:*/data/paste_primary.html
|
||||
|
||||
Scenario: :buffer with wrong argument (-1)
|
||||
Given I have a fresh instance
|
||||
When I open data/title.html
|
||||
And I run :buffer "-1"
|
||||
Then the error "There's no tab with index -1!" should be shown
|
||||
|
@ -17,5 +17,30 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
import pytest_bdd as bdd
|
||||
bdd.scenarios('hints.feature')
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_up_word_hints(tmpdir, quteproc):
|
||||
dict_file = tmpdir / 'dict'
|
||||
dict_file.write(textwrap.dedent("""
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
twelve
|
||||
thirteen
|
||||
"""))
|
||||
quteproc.set_setting('hints', 'dictionary', str(dict_file))
|
||||
|
@ -19,10 +19,3 @@
|
||||
|
||||
import pytest_bdd as bdd
|
||||
bdd.scenarios('tabs.feature')
|
||||
|
||||
|
||||
@bdd.given("I clean up open tabs")
|
||||
def clean_open_tabs(quteproc):
|
||||
quteproc.set_setting('tabs', 'last-close', 'blank')
|
||||
quteproc.send_cmd(':tab-only')
|
||||
quteproc.send_cmd(':tab-close')
|
||||
|
@ -3,7 +3,7 @@ Feature: Yanking and pasting.
|
||||
from/to the clipboard and primary selection.
|
||||
|
||||
Background:
|
||||
Given I run :tab-only
|
||||
Given I clean up open tabs
|
||||
|
||||
#### :yank
|
||||
|
||||
@ -79,9 +79,7 @@ Feature: Yanking and pasting.
|
||||
Then the error "Clipboard is empty." should be shown
|
||||
|
||||
Scenario: Pasting in a new tab
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :open -t {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
@ -89,9 +87,7 @@ Feature: Yanking and pasting.
|
||||
- data/hello.txt (active)
|
||||
|
||||
Scenario: Pasting in a background tab
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :open -b {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
@ -99,7 +95,6 @@ Feature: Yanking and pasting.
|
||||
- data/hello.txt
|
||||
|
||||
Scenario: Pasting in a new window
|
||||
Given I have a fresh instance
|
||||
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :open -w {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
@ -123,7 +118,6 @@ Feature: Yanking and pasting.
|
||||
Then the error "Invalid URL" should be shown
|
||||
|
||||
Scenario: Pasting multiple urls in a new tab
|
||||
Given I have a fresh instance
|
||||
When I put the following lines into the clipboard:
|
||||
http://localhost:(port)/data/hello.txt
|
||||
http://localhost:(port)/data/hello2.txt
|
||||
@ -139,8 +133,8 @@ Feature: Yanking and pasting.
|
||||
- data/hello3.txt
|
||||
|
||||
Scenario: Pasting multiline text
|
||||
Given I have a fresh instance
|
||||
When I set searchengines -> DEFAULT to http://localhost:(port)/data/hello.txt?q={}
|
||||
When I set general -> auto-search to true
|
||||
And I set searchengines -> DEFAULT to http://localhost:(port)/data/hello.txt?q={}
|
||||
And I put the following lines into the clipboard:
|
||||
this url:
|
||||
http://qutebrowser.org
|
||||
@ -152,9 +146,8 @@ Feature: Yanking and pasting.
|
||||
- data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open (active)
|
||||
|
||||
Scenario: Pasting multiline whose first line looks like a URI
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
When I set searchengines -> DEFAULT to http://localhost:(port)/data/hello.txt?q={}
|
||||
When I set general -> auto-search to true
|
||||
And I set searchengines -> DEFAULT to http://localhost:(port)/data/hello.txt?q={}
|
||||
And I put the following lines into the clipboard:
|
||||
text:
|
||||
should open
|
||||
@ -166,9 +159,7 @@ Feature: Yanking and pasting.
|
||||
- data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search (active)
|
||||
|
||||
Scenario: Pasting multiple urls in a background tab
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
And I put the following lines into the clipboard:
|
||||
When I put the following lines into the clipboard:
|
||||
http://localhost:(port)/data/hello.txt
|
||||
http://localhost:(port)/data/hello2.txt
|
||||
http://localhost:(port)/data/hello3.txt
|
||||
@ -183,7 +174,6 @@ Feature: Yanking and pasting.
|
||||
- data/hello3.txt
|
||||
|
||||
Scenario: Pasting multiple urls in new windows
|
||||
Given I have a fresh instance
|
||||
When I put the following lines into the clipboard:
|
||||
http://localhost:(port)/data/hello.txt
|
||||
http://localhost:(port)/data/hello2.txt
|
||||
@ -216,93 +206,68 @@ Feature: Yanking and pasting.
|
||||
url: http://localhost:*/data/hello3.txt
|
||||
|
||||
Scenario: Pasting multiple urls with an empty one
|
||||
When I open about:blank
|
||||
And I put "http://localhost:(port)/data/hello.txt\n\nhttp://localhost:(port)/data/hello2.txt" into the clipboard
|
||||
And I run :open -t {clipboard}
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: Pasting multiple urls with an almost empty one
|
||||
When I open about:blank
|
||||
And I put "http://localhost:(port)/data/hello.txt\n \nhttp://localhost:(port)/data/hello2.txt" into the clipboard
|
||||
And I run :open -t {clipboard}
|
||||
Then no crash should happen
|
||||
|
||||
#### :paste-primary
|
||||
#### :insert-text
|
||||
|
||||
Scenario: Pasting the primary selection into an empty text field
|
||||
When selection is supported
|
||||
And I open data/paste_primary.html
|
||||
And I put "Hello world" into the primary selection
|
||||
Scenario: Inserting text into an empty text field
|
||||
When I open data/paste_primary.html
|
||||
# Click the text field
|
||||
And I run :hint all
|
||||
And I run :follow-hint a
|
||||
And I wait for "Clicked editable element!" in the log
|
||||
And I run :paste-primary
|
||||
And I run :insert-text Hello world
|
||||
# Compare
|
||||
Then the text field should contain "Hello world"
|
||||
|
||||
Scenario: Pasting the primary selection into a text field at specific position
|
||||
When selection is supported
|
||||
And I open data/paste_primary.html
|
||||
Scenario: Inserting text into a text field at specific position
|
||||
When I open data/paste_primary.html
|
||||
And I set the text field to "one two three four"
|
||||
And I put " Hello world" into the primary selection
|
||||
# Click the text field
|
||||
And I run :hint all
|
||||
And I run :follow-hint a
|
||||
And I wait for "Clicked editable element!" in the log
|
||||
# Move to the beginning and two words to the right
|
||||
# Move to the beginning and two characters to the right
|
||||
And I press the keys "<Home>"
|
||||
And I press the key "<Ctrl+Right>"
|
||||
And I press the key "<Ctrl+Right>"
|
||||
And I run :paste-primary
|
||||
And I press the key "<Right>"
|
||||
And I press the key "<Right>"
|
||||
And I run :insert-text Hello world
|
||||
# Compare
|
||||
Then the text field should contain "one two Hello world three four"
|
||||
Then the text field should contain "onHello worlde two three four"
|
||||
|
||||
Scenario: Pasting the primary selection into a text field with undo
|
||||
When selection is supported
|
||||
And I open data/paste_primary.html
|
||||
Scenario: Inserting text into a text field with undo
|
||||
When I open data/paste_primary.html
|
||||
# Click the text field
|
||||
And I run :hint all
|
||||
And I run :follow-hint a
|
||||
And I wait for "Clicked editable element!" in the log
|
||||
# Paste and undo
|
||||
And I put "This text should be undone" into the primary selection
|
||||
And I run :paste-primary
|
||||
And I run :insert-text This text should be undone
|
||||
And I press the key "<Ctrl+z>"
|
||||
# Paste final text
|
||||
And I put "This text should stay" into the primary selection
|
||||
And I run :paste-primary
|
||||
And I run :insert-text This text should stay
|
||||
# Compare
|
||||
Then the text field should contain "This text should stay"
|
||||
|
||||
Scenario: Pasting the primary selection without a focused field
|
||||
When selection is supported
|
||||
And I open data/paste_primary.html
|
||||
And I put "test" into the primary selection
|
||||
Scenario: Inserting text without a focused field
|
||||
When I open data/paste_primary.html
|
||||
And I run :enter-mode insert
|
||||
And I run :paste-primary
|
||||
And I run :insert-text test
|
||||
Then the error "No element focused!" should be shown
|
||||
|
||||
Scenario: Pasting the primary selection with a read-only field
|
||||
When selection is supported
|
||||
And I open data/paste_primary.html
|
||||
Scenario: Inserting text with a read-only field
|
||||
When I open data/paste_primary.html
|
||||
# Click the text field
|
||||
And I run :hint all
|
||||
And I run :follow-hint s
|
||||
And I wait for "Clicked non-editable element!" in the log
|
||||
And I put "test" into the primary selection
|
||||
And I run :enter-mode insert
|
||||
And I run :paste-primary
|
||||
And I run :insert-text test
|
||||
Then the error "Focused element is not editable!" should be shown
|
||||
|
||||
Scenario: :paste-primary without primary selection supported
|
||||
When selection is not supported
|
||||
And I open data/paste_primary.html
|
||||
And I put "Hello world" into the clipboard
|
||||
# Click the text field
|
||||
And I run :hint all
|
||||
And I run :follow-hint a
|
||||
And I wait for "Clicked editable element!" in the log
|
||||
And I run :paste-primary
|
||||
# Compare
|
||||
Then the text field should contain "Hello world"
|
||||
|
@ -113,20 +113,3 @@ def test_word_hints_issue1393(quteproc, tmpdir):
|
||||
quteproc.wait_for(message='hints: *', category='hints')
|
||||
quteproc.send_cmd(':follow-hint {}'.format(hint))
|
||||
quteproc.wait_for_load_finished('data/{}'.format(target))
|
||||
|
||||
|
||||
def test_short_dict(quteproc, tmpdir):
|
||||
dict_file = tmpdir / 'dict'
|
||||
dict_file.write(textwrap.dedent("""
|
||||
worda
|
||||
wordb
|
||||
"""))
|
||||
quteproc.set_setting('hints', 'mode', 'word')
|
||||
quteproc.set_setting('hints', 'dictionary', str(dict_file))
|
||||
quteproc.open_path('data/hints/short_dict.html')
|
||||
quteproc.send_cmd(':hint')
|
||||
line = quteproc.wait_for(message='Not enough words in the dictionary.')
|
||||
line.expected = True
|
||||
quteproc.wait_for(message='hints: *', category='hints')
|
||||
quteproc.send_cmd(':follow-hint d')
|
||||
quteproc.wait_for_load_finished('data/hello3.txt')
|
||||
|
@ -46,7 +46,7 @@ def test_insert_mode(file_name, source, input_text, auto_insert, quteproc):
|
||||
quteproc.press_keys(input_text)
|
||||
elif source == 'clipboard':
|
||||
quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(input_text))
|
||||
quteproc.send_cmd(':paste-primary')
|
||||
quteproc.send_cmd(':insert-text {clipboard}')
|
||||
|
||||
quteproc.send_cmd(':hint all')
|
||||
quteproc.send_cmd(':follow-hint a')
|
||||
|
@ -97,32 +97,56 @@ def test_maybe_resize_completion(completionview, config_stub, qtbot):
|
||||
completionview.maybe_resize_completion()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, count, expected', [
|
||||
([['Aa']], 1, 'Aa'),
|
||||
([['Aa']], -1, 'Aa'),
|
||||
([['Aa'], ['Ba']], 1, 'Aa'),
|
||||
([['Aa'], ['Ba']], -1, 'Ba'),
|
||||
([['Aa'], ['Ba']], 2, 'Ba'),
|
||||
([['Aa'], ['Ba']], -2, 'Aa'),
|
||||
([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ac'),
|
||||
([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ba'),
|
||||
([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 6, 'Ca'),
|
||||
([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 7, 'Aa'),
|
||||
([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -1, 'Ca'),
|
||||
([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -2, 'Bb'),
|
||||
([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -4, 'Ac'),
|
||||
([[], ['Ba', 'Bb']], 1, 'Ba'),
|
||||
([[], ['Ba', 'Bb']], -1, 'Bb'),
|
||||
([[], [], ['Ca', 'Cb']], 1, 'Ca'),
|
||||
([[], [], ['Ca', 'Cb']], -1, 'Cb'),
|
||||
([['Aa'], []], 1, 'Aa'),
|
||||
([['Aa'], []], -1, 'Aa'),
|
||||
([['Aa'], [], []], 1, 'Aa'),
|
||||
([['Aa'], [], []], -1, 'Aa'),
|
||||
([[]], 1, None),
|
||||
([[]], -1, None),
|
||||
@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),
|
||||
])
|
||||
def test_completion_item_focus(tree, count, expected, completionview):
|
||||
def test_completion_item_focus(which, tree, count, expected, completionview):
|
||||
"""Test that on_next_prev_item moves the selection properly.
|
||||
|
||||
Args:
|
||||
@ -140,9 +164,8 @@ def test_completion_item_focus(tree, count, expected, completionview):
|
||||
filtermodel = sortfilter.CompletionFilterModel(model,
|
||||
parent=completionview)
|
||||
completionview.set_model(filtermodel)
|
||||
direction = 'prev' if count < 0 else 'next'
|
||||
for _ in range(abs(count)):
|
||||
completionview.completion_item_focus(direction)
|
||||
for _ in range(count):
|
||||
completionview.completion_item_focus(which)
|
||||
idx = completionview.selectionModel().currentIndex()
|
||||
assert filtermodel.data(idx) == expected
|
||||
|
||||
|
@ -620,6 +620,33 @@ class TestIncDecNumber:
|
||||
base_url, incdec, segments={'host', 'path', 'query', 'anchor'})
|
||||
assert new_url == expected_url
|
||||
|
||||
@pytest.mark.parametrize('incdec', ['increment', 'decrement'])
|
||||
@pytest.mark.parametrize('value', [
|
||||
'{}foo', 'foo{}', 'foo{}bar', '42foo{}'
|
||||
])
|
||||
@pytest.mark.parametrize('url', [
|
||||
'http://example.com:80/v1/path/{}/test',
|
||||
'http://example.com:80/v1/query_test?value={}',
|
||||
'http://example.com:80/v1/anchor_test#{}',
|
||||
'http://host_{}_test.com:80',
|
||||
'http://m4ny.c0m:80/number5/3very?where=yes#{}'
|
||||
])
|
||||
@pytest.mark.parametrize('count', [1, 5, 100])
|
||||
def test_incdec_number_count(self, incdec, value, url, count):
|
||||
"""Test incdec_number with valid URLs and a count."""
|
||||
base_value = value.format(20)
|
||||
if incdec == 'increment':
|
||||
expected_value = value.format(20 + count)
|
||||
else:
|
||||
expected_value = value.format(20 - count)
|
||||
|
||||
base_url = QUrl(url.format(base_value))
|
||||
expected_url = QUrl(url.format(expected_value))
|
||||
new_url = urlutils.incdec_number(
|
||||
base_url, incdec, count,
|
||||
segments={'host', 'path', 'query', 'anchor'})
|
||||
assert new_url == expected_url
|
||||
|
||||
@pytest.mark.parametrize('number, expected, incdec', [
|
||||
('01', '02', 'increment'),
|
||||
('09', '10', 'increment'),
|
||||
|
Loading…
Reference in New Issue
Block a user