This commit is contained in:
Jan Verbeek 2016-08-16 14:05:04 +02:00
commit cfd166a95e
33 changed files with 653 additions and 279 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
check-manifest==0.31
check-manifest==0.32

View File

@ -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

View File

@ -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.

View File

@ -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):

View File

@ -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

View File

@ -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
"""

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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}'),
]

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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 = []

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View 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')

View File

@ -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"

View File

@ -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')

View File

@ -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')

View File

@ -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

View File

@ -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'),