Merge https://github.com/The-Compiler/qutebrowser into clip
Also make it possible to use multiple variables in one argument.
This commit is contained in:
commit
ebfe23c376
@ -44,6 +44,17 @@ Changed
|
||||
(i.e. to open it at the position it would be opened if it was a clicked link)
|
||||
- `:download-open` and `:prompt-open-download` now have an optional `cmdline`
|
||||
argument to pass a commandline to open the download with.
|
||||
- `:yank` now has a position argument to select what to yank instead of using
|
||||
flags.
|
||||
- Replacements like `{url}` can now also be used in the middle of an argument.
|
||||
Consequently, commands taking another command (`:later`, `:repeat` and
|
||||
`:bind`) now don't immediately evaluate variables.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `:yank-selected` command got merged into `:yank` as `:yank selection`
|
||||
and thus removed.
|
||||
|
||||
v0.8.3 (unreleased)
|
||||
-------------------
|
||||
@ -52,6 +63,7 @@ Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed crash when doing `:<space><enter>`, another corner-case introduced in v0.8.0
|
||||
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows
|
||||
|
||||
v0.8.2
|
||||
------
|
||||
|
@ -146,13 +146,13 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Lamar Pavel
|
||||
* Bruno Oliveira
|
||||
* Alexander Cogneau
|
||||
* Marshall Lochbaum
|
||||
* Felix Van der Jeugt
|
||||
* Jakub Klinkovský
|
||||
* Martin Tournoij
|
||||
* Marshall Lochbaum
|
||||
* Jan Verbeek
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Jan Verbeek
|
||||
* Patric Schmitz
|
||||
* Tarcisio Fedrizzi
|
||||
* Claude
|
||||
|
@ -65,8 +65,7 @@
|
||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|<<view-source,view-source>>|Show the source of the current page.
|
||||
|<<wq,wq>>|Save open pages and quit.
|
||||
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
||||
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|
||||
|<<yank,yank>>|Yank something to the clipboard or primary selection.
|
||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|
||||
|<<zoom-out,zoom-out>>|Decrease the zoom level for the current tab.
|
||||
@ -110,6 +109,7 @@ Bind a key to a command.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
* This command does not replace variables like +\{url\}+.
|
||||
|
||||
[[bookmark-add]]
|
||||
=== bookmark-add
|
||||
@ -424,6 +424,7 @@ Execute a command after some time.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
* This command does not replace variables like +\{url\}+.
|
||||
|
||||
[[messages]]
|
||||
=== messages
|
||||
@ -579,6 +580,7 @@ Repeat a given command.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
* This command does not replace variables like +\{url\}+.
|
||||
|
||||
[[report]]
|
||||
=== report
|
||||
@ -840,25 +842,25 @@ Save open pages and quit.
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--title*] [*--sel*] [*--domain*] [*--pretty*]+
|
||||
Syntax: +:yank [*--sel*] [*--keep*] ['what']+
|
||||
|
||||
Yank the current URL/title to the clipboard or primary selection.
|
||||
Yank something to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--title*+: Yank the title instead of the URL.
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
|
||||
* +*-p*+, +*--pretty*+: Yank the URL in pretty decoded form.
|
||||
==== positional arguments
|
||||
* +'what'+: What to yank.
|
||||
|
||||
- `url`: The current URL.
|
||||
- `pretty-url`: The URL in pretty decoded form.
|
||||
- `title`: The current page's title.
|
||||
- `domain`: The current scheme, domain, and port number.
|
||||
- `selection`: The selection under the cursor.
|
||||
|
||||
|
||||
[[yank-selected]]
|
||||
=== yank-selected
|
||||
Syntax: +:yank-selected [*--sel*] [*--keep*]+
|
||||
|
||||
Yank the selected text to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
|
||||
* +*-k*+, +*--keep*+: Stay in visual mode after yanking the selection.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
|
@ -1,2 +1,2 @@
|
||||
pip==8.1.2
|
||||
setuptools==25.1.4
|
||||
setuptools==25.1.6
|
||||
|
@ -3,4 +3,4 @@
|
||||
pluggy==0.3.1
|
||||
py==1.4.31
|
||||
tox==2.3.1
|
||||
virtualenv==15.0.2
|
||||
virtualenv==15.0.3
|
||||
|
@ -645,30 +645,44 @@ class CommandDispatcher:
|
||||
"representation.")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def yank(self, title=False, sel=False, domain=False, pretty=False):
|
||||
"""Yank the current URL/title to the clipboard or primary selection.
|
||||
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
|
||||
'title', 'domain'])
|
||||
def yank(self, what='url', sel=False, keep=False):
|
||||
"""Yank something to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
what: What to yank.
|
||||
|
||||
- `url`: The current URL.
|
||||
- `pretty-url`: The URL in pretty decoded form.
|
||||
- `title`: The current page's title.
|
||||
- `domain`: The current scheme, domain, and port number.
|
||||
- `selection`: The selection under the cursor.
|
||||
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
title: Yank the title instead of the URL.
|
||||
domain: Yank only the scheme, domain, and port number.
|
||||
pretty: Yank the URL in pretty decoded form.
|
||||
keep: Stay in visual mode after yanking the selection.
|
||||
"""
|
||||
if title:
|
||||
if what == 'title':
|
||||
s = self._tabbed_browser.page_title(self._current_index())
|
||||
what = 'title'
|
||||
elif domain:
|
||||
elif what == 'domain':
|
||||
port = self._current_url().port()
|
||||
s = '{}://{}{}'.format(self._current_url().scheme(),
|
||||
self._current_url().host(),
|
||||
':' + str(port) if port > -1 else '')
|
||||
what = 'domain'
|
||||
else:
|
||||
elif what in ['url', 'pretty-url']:
|
||||
flags = QUrl.RemovePassword
|
||||
if not pretty:
|
||||
if what != 'pretty-url':
|
||||
flags |= QUrl.FullyEncoded
|
||||
s = self._current_url().toString(flags)
|
||||
what = 'URL'
|
||||
what = 'URL' # For printing
|
||||
elif what == 'selection':
|
||||
caret = self._current_widget().caret
|
||||
s = caret.selection()
|
||||
if not caret.has_selection() or not s:
|
||||
message.info(self._win_id, "Nothing to yank")
|
||||
return
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Invalid value {!r} for `what'.".format(what))
|
||||
|
||||
if sel and utils.supports_selection():
|
||||
target = "primary selection"
|
||||
@ -677,8 +691,15 @@ class CommandDispatcher:
|
||||
target = "clipboard"
|
||||
|
||||
utils.set_clipboard(s, selection=sel)
|
||||
message.info(self._win_id, "Yanked {} to {}: {}".format(
|
||||
what, target, s))
|
||||
if what != 'selection':
|
||||
message.info(self._win_id, "Yanked {} to {}: {}".format(
|
||||
what, target, s))
|
||||
else:
|
||||
message.info(self._win_id, "{} {} yanked to {}".format(
|
||||
len(s), "char" if len(s) == 1 else "chars", target))
|
||||
if not keep:
|
||||
modeman.maybe_leave(self._win_id, KeyMode.caret,
|
||||
"yank selected")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@ -982,7 +1003,7 @@ class CommandDispatcher:
|
||||
self._tabbed_browser.setUpdatesEnabled(True)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
maxsplit=0, no_replace_variables=True)
|
||||
def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
|
||||
"""Spawn a command in a shell.
|
||||
|
||||
@ -1755,31 +1776,6 @@ class CommandDispatcher:
|
||||
"""Move the cursor or selection to the end of the document."""
|
||||
self._current_widget().caret.move_to_end_of_document()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def yank_selected(self, sel=False, keep=False):
|
||||
"""Yank the selected text to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
keep: If given, stay in visual mode after yanking.
|
||||
"""
|
||||
caret = self._current_widget().caret
|
||||
s = caret.selection()
|
||||
if not caret.has_selection() or len(s) == 0:
|
||||
message.info(self._win_id, "Nothing to yank")
|
||||
return
|
||||
|
||||
if sel and utils.supports_selection():
|
||||
target = "primary selection"
|
||||
else:
|
||||
sel = False
|
||||
target = "clipboard"
|
||||
utils.set_clipboard(s, sel)
|
||||
message.info(self._win_id, "{} {} yanked to {}".format(
|
||||
len(s), "char" if len(s) == 1 else "chars", target))
|
||||
if not keep:
|
||||
modeman.maybe_leave(self._win_id, KeyMode.caret, "yank selected")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
modes=[KeyMode.caret], scope='window')
|
||||
def toggle_selection(self):
|
||||
|
@ -75,13 +75,13 @@ class Command:
|
||||
deprecated: False, or a string to describe why a command is deprecated.
|
||||
desc: The description of the command.
|
||||
handler: The handler function to call.
|
||||
completion: Completions to use for arguments, as a list of strings.
|
||||
debug: Whether this is a debugging command (only shown with --debug).
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
flags_with_args: A list of flags which take an argument.
|
||||
no_cmd_split: If true, ';;' to split sub-commands is ignored.
|
||||
backend: Which backend the command works with (or None if it works with
|
||||
both)
|
||||
no_replace_variables: Don't replace variables like {url}
|
||||
_qute_args: The saved data from @cmdutils.argument
|
||||
_needs_js: Whether the command needs javascript enabled
|
||||
_modes: The modes the command can be executed in.
|
||||
@ -95,7 +95,7 @@ class Command:
|
||||
hide=False, modes=None, not_modes=None, needs_js=False,
|
||||
debug=False, ignore_args=False, deprecated=False,
|
||||
no_cmd_split=False, star_args_optional=False, scope='global',
|
||||
backend=None):
|
||||
backend=None, no_replace_variables=False):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-locals
|
||||
if modes is not None and not_modes is not None:
|
||||
@ -127,6 +127,7 @@ class Command:
|
||||
self.handler = handler
|
||||
self.no_cmd_split = no_cmd_split
|
||||
self.backend = backend
|
||||
self.no_replace_variables = no_replace_variables
|
||||
|
||||
self.docparser = docutils.DocstringParser(handler)
|
||||
self.parser = argparser.ArgumentParser(
|
||||
@ -148,13 +149,7 @@ class Command:
|
||||
self._qute_args = getattr(self.handler, 'qute_args', {})
|
||||
self.handler.qute_args = None
|
||||
|
||||
args = self._inspect_func()
|
||||
|
||||
self.completion = []
|
||||
for arg in args:
|
||||
arg_completion = self.get_arg_info(arg).completion
|
||||
if arg_completion is not None:
|
||||
self.completion.append(arg_completion)
|
||||
self._inspect_func()
|
||||
|
||||
def _check_prerequisites(self, win_id):
|
||||
"""Check if the command is permitted to run currently.
|
||||
@ -208,6 +203,11 @@ class Command:
|
||||
"""Get an ArgInfo tuple for the given inspect.Parameter."""
|
||||
return self._qute_args.get(param.name, ArgInfo())
|
||||
|
||||
def get_pos_arg_info(self, pos):
|
||||
"""Get an ArgInfo tuple for the given positional parameter."""
|
||||
name = self.pos_args[pos][0]
|
||||
return self._qute_args.get(name, ArgInfo())
|
||||
|
||||
def _inspect_special_param(self, param):
|
||||
"""Check if the given parameter is a special one.
|
||||
|
||||
|
@ -52,29 +52,28 @@ def replace_variables(win_id, arglist):
|
||||
args = []
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if '{url}' in arglist:
|
||||
if any('{url}' in arg for arg in arglist):
|
||||
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded |
|
||||
QUrl.RemovePassword)
|
||||
if '{url:pretty}' in arglist:
|
||||
if any('{url:pretty}' in arg for arg in arglist):
|
||||
pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword)
|
||||
try:
|
||||
if '{clipboard}' in arglist:
|
||||
if any('{clipboard}' in arg for arg in arglist):
|
||||
clipboard = utils.get_clipboard()
|
||||
if '{primary}' in arglist:
|
||||
if any('{primary}' in arg for arg in arglist):
|
||||
primary = utils.get_clipboard(selection=True)
|
||||
except utils.ClipboardEmptyError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
for arg in arglist:
|
||||
if arg == '{url}':
|
||||
args.append(url)
|
||||
elif arg == '{url:pretty}':
|
||||
args.append(pretty_url)
|
||||
elif arg == '{clipboard}':
|
||||
args.append(clipboard)
|
||||
elif arg == '{primary}':
|
||||
args.append(primary)
|
||||
else:
|
||||
args.append(arg)
|
||||
if '{url}' in arg:
|
||||
arg = arg.replace('{url}', url)
|
||||
if '{url:pretty}' in arg:
|
||||
arg = arg.replace('{url:pretty}', pretty_url)
|
||||
if '{clipboard}' in arg:
|
||||
arg = arg.replace('{clipboard}', clipboard)
|
||||
if '{primary}' in arg:
|
||||
arg = arg.replace('{primary}', primary)
|
||||
args.append(arg)
|
||||
return args
|
||||
|
||||
|
||||
@ -290,7 +289,10 @@ class CommandRunner(QObject):
|
||||
window=self._win_id)
|
||||
cur_mode = mode_manager.mode
|
||||
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if result.cmd.no_replace_variables:
|
||||
args = result.args
|
||||
else:
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if count is not None:
|
||||
if result.count is not None:
|
||||
raise cmdexc.CommandMetaError("Got count via command and "
|
||||
|
@ -204,25 +204,18 @@ class Completer(QObject):
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
# delegate completion to command
|
||||
try:
|
||||
completions = cmdutils.cmd_dict[parts[0]].completion
|
||||
cmd = cmdutils.cmd_dict[parts[0]]
|
||||
except KeyError:
|
||||
# entering an unknown command
|
||||
return None
|
||||
if completions is None:
|
||||
# command without any available completions
|
||||
return None
|
||||
dbg_completions = [c.name for c in completions]
|
||||
try:
|
||||
idx = cursor_part - 1
|
||||
completion = completions[idx]
|
||||
completion = cmd.get_pos_arg_info(idx).completion
|
||||
except IndexError:
|
||||
# More arguments than completions
|
||||
log.completion.debug("completions: {}".format(
|
||||
', '.join(dbg_completions)))
|
||||
# user provided more positional arguments than the command takes
|
||||
return None
|
||||
if completion is None:
|
||||
return None
|
||||
dbg_completions[idx] = '*' + dbg_completions[idx] + '*'
|
||||
log.completion.debug("completions: {}".format(
|
||||
', '.join(dbg_completions)))
|
||||
model = self._get_completion_model(completion, parts, cursor_part)
|
||||
return model
|
||||
|
||||
|
@ -27,8 +27,7 @@ Module attributes:
|
||||
|
||||
import functools
|
||||
|
||||
from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel,
|
||||
base)
|
||||
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
|
||||
from qutebrowser.utils import objreg, usertypes, log, debug
|
||||
from qutebrowser.config import configdata
|
||||
|
||||
@ -115,13 +114,6 @@ def init_session_completion():
|
||||
_instances[usertypes.Completion.sessions] = model
|
||||
|
||||
|
||||
def _init_empty_completion():
|
||||
"""Initialize empty completion model."""
|
||||
log.completion.debug("Initializing empty completion.")
|
||||
if usertypes.Completion.empty not in _instances:
|
||||
_instances[usertypes.Completion.empty] = base.BaseCompletionModel()
|
||||
|
||||
|
||||
INITIALIZERS = {
|
||||
usertypes.Completion.command: _init_command_completion,
|
||||
usertypes.Completion.helptopic: _init_helptopic_completion,
|
||||
@ -133,7 +125,6 @@ INITIALIZERS = {
|
||||
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
|
||||
usertypes.Completion.bookmark_by_url: init_bookmark_completions,
|
||||
usertypes.Completion.sessions: init_session_completion,
|
||||
usertypes.Completion.empty: _init_empty_completion,
|
||||
}
|
||||
|
||||
|
||||
|
@ -135,8 +135,8 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
|
||||
for col in self.srcmodel.columns_to_filter:
|
||||
idx = self.srcmodel.index(row, col, parent)
|
||||
if not idx.isValid():
|
||||
# No entries in parent model
|
||||
if not idx.isValid(): # pragma: no cover
|
||||
# this is a sanity check not hit by any test case
|
||||
continue
|
||||
data = self.srcmodel.data(idx)
|
||||
if not data:
|
||||
|
@ -1506,12 +1506,12 @@ KEY_DATA = collections.OrderedDict([
|
||||
('enter-mode jump_mark', ["'"]),
|
||||
('yank', ['yy']),
|
||||
('yank -s', ['yY']),
|
||||
('yank -t', ['yt']),
|
||||
('yank -ts', ['yT']),
|
||||
('yank -d', ['yd']),
|
||||
('yank -ds', ['yD']),
|
||||
('yank -p', ['yp']),
|
||||
('yank -ps', ['yP']),
|
||||
('yank title', ['yt']),
|
||||
('yank title -s', ['yT']),
|
||||
('yank domain', ['yd']),
|
||||
('yank domain -s', ['yD']),
|
||||
('yank pretty-url', ['yp']),
|
||||
('yank pretty-url -s', ['yP']),
|
||||
('open {clipboard}', ['pp']),
|
||||
('open {primary}', ['pP']),
|
||||
('open -t {clipboard}', ['Pp']),
|
||||
@ -1638,8 +1638,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('move-to-end-of-line', ['$']),
|
||||
('move-to-start-of-document', ['gg']),
|
||||
('move-to-end-of-document', ['G']),
|
||||
('yank-selected -p', ['Y']),
|
||||
('yank-selected', ['y'] + RETURN_KEYS),
|
||||
('yank selection -s', ['Y']),
|
||||
('yank selection', ['y'] + RETURN_KEYS),
|
||||
('scroll left', ['H']),
|
||||
('scroll down', ['J']),
|
||||
('scroll up', ['K']),
|
||||
@ -1678,6 +1678,15 @@ CHANGED_KEY_COMMANDS = [
|
||||
|
||||
(re.compile(r'^hint links fill "([^"]*)"$'), r'hint links fill \1'),
|
||||
|
||||
(re.compile(r'^yank -t(\S+)'), r'yank title -\1'),
|
||||
(re.compile(r'^yank -t'), r'yank title'),
|
||||
(re.compile(r'^yank -d(\S+)'), r'yank domain -\1'),
|
||||
(re.compile(r'^yank -d'), r'yank domain'),
|
||||
(re.compile(r'^yank -p(\S+)'), r'yank pretty-url -\1'),
|
||||
(re.compile(r'^yank -p'), r'yank pretty-url'),
|
||||
(re.compile(r'^yank-selected -p'), r'yank selection -s'),
|
||||
(re.compile(r'^yank-selected'), r'yank selection'),
|
||||
|
||||
(re.compile(r'^paste$'), r'open {clipboard}'),
|
||||
(re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'),
|
||||
(re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'),
|
||||
|
@ -150,9 +150,9 @@ class KeyConfigParser(QObject):
|
||||
data = str(self)
|
||||
f.write(data)
|
||||
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True)
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
|
||||
no_replace_variables=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@cmdutils.argument('key', completion=usertypes.Completion.empty)
|
||||
@cmdutils.argument('command', completion=usertypes.Completion.command)
|
||||
def bind(self, key, win_id, command=None, *, mode='normal', force=False):
|
||||
"""Bind a key to a command.
|
||||
@ -361,12 +361,12 @@ class KeyConfigParser(QObject):
|
||||
raise KeyConfigError("Got command '{}' without getting a "
|
||||
"section!".format(line))
|
||||
else:
|
||||
self._validate_command(line)
|
||||
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
|
||||
if rgx.match(line):
|
||||
line = rgx.sub(repl, line)
|
||||
self._mark_config_dirty()
|
||||
break
|
||||
self._validate_command(line)
|
||||
self._cur_command = line
|
||||
|
||||
def _read_keybinding(self, line):
|
||||
|
@ -23,8 +23,8 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.commands import cmdexc, cmdutils, runners
|
||||
from qutebrowser.misc import cmdhistory, split
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.misc import cmdhistory
|
||||
from qutebrowser.misc import miscwidgets as misc
|
||||
from qutebrowser.utils import usertypes, log, objreg
|
||||
|
||||
@ -108,10 +108,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
space: If given, a space is added to the end.
|
||||
append: If given, the text is appended to the current text.
|
||||
"""
|
||||
args = split.simple_split(text)
|
||||
args = runners.replace_variables(self._win_id, args)
|
||||
text = ' '.join(args)
|
||||
|
||||
if space:
|
||||
text += ' '
|
||||
if append:
|
||||
|
@ -35,8 +35,8 @@ class ExternalEditor(QObject):
|
||||
|
||||
Attributes:
|
||||
_text: The current text before the editor is opened.
|
||||
_oshandle: The OS level handle to the tmpfile.
|
||||
_filehandle: The file handle to the tmpfile.
|
||||
_file: The file handle as tempfile.NamedTemporaryFile. Note that this
|
||||
handle will be closed after the initial file has been created.
|
||||
_proc: The GUIProcess of the editor.
|
||||
_win_id: The window ID the ExternalEditor is associated with.
|
||||
"""
|
||||
@ -46,20 +46,18 @@ class ExternalEditor(QObject):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._text = None
|
||||
self._oshandle = None
|
||||
self._filename = None
|
||||
self._file = None
|
||||
self._proc = None
|
||||
self._win_id = win_id
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up temporary files after the editor closed."""
|
||||
if self._oshandle is None or self._filename is None:
|
||||
if self._file is None:
|
||||
# Could not create initial file.
|
||||
return
|
||||
try:
|
||||
os.close(self._oshandle)
|
||||
if self._proc.exit_status() != QProcess.CrashExit:
|
||||
os.remove(self._filename)
|
||||
os.remove(self._file.name)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
@ -82,7 +80,7 @@ class ExternalEditor(QObject):
|
||||
return
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
try:
|
||||
with open(self._filename, 'r', encoding=encoding) as f:
|
||||
with open(self._file.name, 'r', encoding=encoding) as f:
|
||||
text = f.read()
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
@ -108,13 +106,18 @@ class ExternalEditor(QObject):
|
||||
if self._text is not None:
|
||||
raise ValueError("Already editing a file!")
|
||||
self._text = text
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
try:
|
||||
self._oshandle, self._filename = tempfile.mkstemp(
|
||||
text=True, prefix='qutebrowser-editor-')
|
||||
if text:
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
with open(self._filename, 'w', encoding=encoding) as f:
|
||||
f.write(text)
|
||||
# Close while the external process is running, as otherwise systems
|
||||
# with exclusive write access (e.g. Windows) may fail to update
|
||||
# the file from the external editor, see
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1767
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w', prefix='qutebrowser-editor-', encoding=encoding,
|
||||
delete=False) as fobj:
|
||||
if text:
|
||||
fobj.write(text)
|
||||
self._file = fobj
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Failed to create initial file: "
|
||||
"{}".format(e))
|
||||
@ -125,6 +128,6 @@ class ExternalEditor(QObject):
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
editor = config.get('general', 'editor')
|
||||
executable = editor[0]
|
||||
args = [arg.replace('{}', self._filename) for arg in editor[1:]]
|
||||
args = [arg.replace('{}', self._file.name) for arg in editor[1:]]
|
||||
log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
|
||||
self._proc.start(executable, args)
|
||||
|
@ -39,7 +39,7 @@ from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True)
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def later(ms: int, command, win_id):
|
||||
"""Execute a command after some time.
|
||||
@ -69,7 +69,7 @@ def later(ms: int, command, win_id):
|
||||
raise
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True)
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def repeat(times: int, command, win_id):
|
||||
"""Repeat a given command.
|
||||
|
@ -238,8 +238,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
# Available command completions
|
||||
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
'helptopic', 'quickmark_by_name',
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions',
|
||||
'empty'])
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions'])
|
||||
|
||||
|
||||
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||
|
@ -150,12 +150,14 @@ PERFECT_FILES = [
|
||||
|
||||
('tests/unit/completion/test_models.py',
|
||||
'qutebrowser/completion/models/base.py'),
|
||||
('tests/unit/completion/test_sortfilter.py',
|
||||
'qutebrowser/completion/models/sortfilter.py'),
|
||||
|
||||
]
|
||||
|
||||
|
||||
# 100% coverage because of end2end tests, but no perfect unit tests yet.
|
||||
WHITELISTED_FILES = []
|
||||
WHITELISTED_FILES = ['qutebrowser/browser/webkit/webkitinspector.py']
|
||||
|
||||
|
||||
class Skipped(Exception):
|
||||
|
@ -239,7 +239,8 @@ def _get_command_doc_notes(cmd):
|
||||
Yield:
|
||||
Strings which should be added to the docs.
|
||||
"""
|
||||
if cmd.maxsplit is not None or cmd.no_cmd_split:
|
||||
if (cmd.maxsplit is not None or cmd.no_cmd_split or
|
||||
cmd.no_replace_variables and cmd.name != "spawn"):
|
||||
yield ""
|
||||
yield "==== note"
|
||||
if cmd.maxsplit is not None:
|
||||
@ -248,6 +249,8 @@ def _get_command_doc_notes(cmd):
|
||||
if cmd.no_cmd_split:
|
||||
yield ("* With this command, +;;+ is interpreted literally "
|
||||
"instead of splitting off a second command.")
|
||||
if cmd.no_replace_variables and cmd.name != "spawn":
|
||||
yield r"* This command does not replace variables like +\{url\}+."
|
||||
|
||||
|
||||
def _get_action_metavar(action, nargs=1):
|
||||
|
@ -10,7 +10,7 @@ Feature: Caret mode
|
||||
Scenario: Selecting the entire document
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-document
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
one two three
|
||||
eins zwei drei
|
||||
@ -23,14 +23,14 @@ Feature: Caret mode
|
||||
And I run :move-to-start-of-document
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Moving to end and to start of document (with selection)
|
||||
When I run :move-to-end-of-document
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-start-of-document
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
one two three
|
||||
eins zwei drei
|
||||
@ -43,7 +43,7 @@ Feature: Caret mode
|
||||
Scenario: Selecting a block
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-next-block
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
one two three
|
||||
eins zwei drei
|
||||
@ -53,7 +53,7 @@ Feature: Caret mode
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-prev-block
|
||||
And I run :move-to-prev-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
drei
|
||||
|
||||
@ -64,14 +64,14 @@ Feature: Caret mode
|
||||
And I run :move-to-end-of-prev-block
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-prev-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "drei"
|
||||
|
||||
Scenario: Moving back to the start of previous block (with selection)
|
||||
When I run :move-to-end-of-next-block with count 2
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-start-of-prev-block
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
eins zwei drei
|
||||
|
||||
@ -82,20 +82,20 @@ Feature: Caret mode
|
||||
And I run :move-to-start-of-prev-block
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-next-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "eins "
|
||||
|
||||
Scenario: Moving to the start of next block (with selection)
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-start-of-next-block
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
Scenario: Moving to the start of next block
|
||||
When I run :move-to-start-of-next-block
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "eins"
|
||||
|
||||
# line
|
||||
@ -103,20 +103,20 @@ Feature: Caret mode
|
||||
Scenario: Selecting a line
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three"
|
||||
|
||||
Scenario: Moving and selecting a line
|
||||
When I run :move-to-next-line
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "eins zwei drei"
|
||||
|
||||
Scenario: Selecting next line
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-next-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
Scenario: Moving to end and to start of line
|
||||
@ -124,21 +124,21 @@ Feature: Caret mode
|
||||
And I run :move-to-start-of-line
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Selecting a line (backwards)
|
||||
When I run :move-to-end-of-line
|
||||
And I run :toggle-selection
|
||||
When I run :move-to-start-of-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three"
|
||||
|
||||
Scenario: Selecting previous line
|
||||
When I run :move-to-next-line
|
||||
And I run :toggle-selection
|
||||
When I run :move-to-prev-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
Scenario: Moving to previous line
|
||||
@ -146,7 +146,7 @@ Feature: Caret mode
|
||||
When I run :move-to-prev-line
|
||||
And I run :toggle-selection
|
||||
When I run :move-to-next-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
# word
|
||||
@ -154,35 +154,35 @@ Feature: Caret mode
|
||||
Scenario: Selecting a word
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Moving to end and selecting a word
|
||||
When I run :move-to-end-of-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain " two"
|
||||
|
||||
Scenario: Moving to next word and selecting a word
|
||||
When I run :move-to-next-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "two"
|
||||
|
||||
Scenario: Moving to next word and selecting until next word
|
||||
When I run :move-to-next-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-next-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "two "
|
||||
|
||||
Scenario: Moving to previous word and selecting a word
|
||||
When I run :move-to-end-of-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-prev-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Moving to previous word
|
||||
@ -190,7 +190,7 @@ Feature: Caret mode
|
||||
And I run :move-to-prev-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
# char
|
||||
@ -198,21 +198,21 @@ Feature: Caret mode
|
||||
Scenario: Selecting a char
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-next-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "o"
|
||||
|
||||
Scenario: Moving and selecting a char
|
||||
When I run :move-to-next-char
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-next-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "n"
|
||||
|
||||
Scenario: Selecting previous char
|
||||
When I run :move-to-end-of-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-prev-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "e"
|
||||
|
||||
Scenario: Moving to previous char
|
||||
@ -220,41 +220,41 @@ Feature: Caret mode
|
||||
And I run :move-to-prev-char
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "e"
|
||||
|
||||
# :yank-selected
|
||||
# :yank selection
|
||||
|
||||
Scenario: :yank-selected without selection
|
||||
When I run :yank-selected
|
||||
Scenario: :yank selection without selection
|
||||
When I run :yank selection
|
||||
Then the message "Nothing to yank" should be shown.
|
||||
|
||||
Scenario: :yank-selected message
|
||||
Scenario: :yank selection message
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the message "3 chars yanked to clipboard" should be shown.
|
||||
|
||||
Scenario: :yank-selected message with one char
|
||||
Scenario: :yank selection message with one char
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-next-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the message "1 char yanked to clipboard" should be shown.
|
||||
|
||||
Scenario: :yank-selected with primary selection
|
||||
Scenario: :yank selection with primary selection
|
||||
When selection is supported
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected --sel
|
||||
And I run :yank selection --sel
|
||||
Then the message "3 chars yanked to primary selection" should be shown.
|
||||
And the primary selection should contain "one"
|
||||
|
||||
Scenario: :yank-selected with --keep
|
||||
Scenario: :yank selection with --keep
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected --keep
|
||||
And I run :yank selection --keep
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected --keep
|
||||
And I run :yank selection --keep
|
||||
Then the message "3 chars yanked to clipboard" should be shown.
|
||||
And the message "7 chars yanked to clipboard" should be shown.
|
||||
And the clipboard should contain "one two"
|
||||
@ -265,7 +265,7 @@ Feature: Caret mode
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :drop-selection
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the message "Nothing to yank" should be shown.
|
||||
|
||||
# :follow-selected
|
||||
|
@ -9,19 +9,19 @@ Feature: Searching on a page
|
||||
|
||||
Scenario: Searching text
|
||||
When I run :search foo
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Searching twice
|
||||
When I run :search foo
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Bar"
|
||||
|
||||
Scenario: Searching with --reverse
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search -r foo
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
Scenario: Searching without matches
|
||||
@ -32,13 +32,13 @@ Feature: Searching on a page
|
||||
Scenario: Searching with / and spaces at the end (issue 874)
|
||||
When I run :set-cmd-text -s /space
|
||||
And I run :command-accept
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "space "
|
||||
|
||||
Scenario: Searching with / and slash in search term (issue 507)
|
||||
When I run :set-cmd-text -s //slash
|
||||
And I run :command-accept
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "/slash"
|
||||
|
||||
# This doesn't work because this is QtWebKit behavior.
|
||||
@ -52,25 +52,25 @@ Feature: Searching on a page
|
||||
Scenario: Searching text with ignore-case = true
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Bar"
|
||||
|
||||
Scenario: Searching text with ignore-case = false
|
||||
When I set general -> ignore-case to false
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "bar"
|
||||
|
||||
Scenario: Searching text with ignore-case = smart (lower-case)
|
||||
When I set general -> ignore-case to smart
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Bar"
|
||||
|
||||
Scenario: Searching text with ignore-case = smart (upper-case)
|
||||
When I set general -> ignore-case to smart
|
||||
And I run :search Foo
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo" # even though foo was first
|
||||
|
||||
## :search-next
|
||||
@ -79,21 +79,21 @@ Feature: Searching on a page
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search foo
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
Scenario: Jumping to next match with count
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search baz
|
||||
And I run :search-next with count 2
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "BAZ"
|
||||
|
||||
Scenario: Jumping to next match with --reverse
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search --reverse foo
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Jumping to next match without search
|
||||
@ -107,7 +107,7 @@ Feature: Searching on a page
|
||||
And I run :search foo
|
||||
And I run :tab-prev
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
## :search-prev
|
||||
@ -117,7 +117,7 @@ Feature: Searching on a page
|
||||
And I run :search foo
|
||||
And I run :search-next
|
||||
And I run :search-prev
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Jumping to previous match with count
|
||||
@ -126,7 +126,7 @@ Feature: Searching on a page
|
||||
And I run :search-next
|
||||
And I run :search-next
|
||||
And I run :search-prev with count 2
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "baz"
|
||||
|
||||
Scenario: Jumping to previous match with --reverse
|
||||
@ -134,7 +134,7 @@ Feature: Searching on a page
|
||||
And I run :search --reverse foo
|
||||
And I run :search-next
|
||||
And I run :search-prev
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
Scenario: Jumping to previous match without search
|
||||
@ -149,14 +149,14 @@ Feature: Searching on a page
|
||||
When I run :search foo
|
||||
And I run :search-next
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Wrapping around page with --reverse
|
||||
When I run :search --reverse foo
|
||||
And I run :search-next
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
# TODO: wrapping message with scrolling
|
||||
|
@ -23,13 +23,13 @@ Feature: Yanking and pasting.
|
||||
Scenario: Yanking title to clipboard
|
||||
When I open data/title.html
|
||||
And I wait for regex "Changing title for idx \d to 'Test title'" in the log
|
||||
And I run :yank --title
|
||||
And I run :yank title
|
||||
Then the message "Yanked title to clipboard: Test title" should be shown
|
||||
And the clipboard should contain "Test title"
|
||||
|
||||
Scenario: Yanking domain to clipboard
|
||||
When I open data/title.html
|
||||
And I run :yank --domain
|
||||
And I run :yank domain
|
||||
Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown
|
||||
And the clipboard should contain "http://localhost:(port)"
|
||||
|
||||
@ -41,7 +41,7 @@ Feature: Yanking and pasting.
|
||||
|
||||
Scenario: Yanking pretty decoded URL
|
||||
When I open data/title with spaces.html
|
||||
And I run :yank --pretty
|
||||
And I run :yank pretty-url
|
||||
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title with spaces.html" should be shown
|
||||
And the clipboard should contain "http://localhost:(port)/data/title with spaces.html"
|
||||
|
||||
|
@ -54,7 +54,7 @@ def test_insert_mode(file_name, source, input_text, auto_insert, quteproc):
|
||||
quteproc.send_cmd(':enter-mode caret')
|
||||
quteproc.send_cmd(':toggle-selection')
|
||||
quteproc.send_cmd(':move-to-prev-word')
|
||||
quteproc.send_cmd(':yank-selected')
|
||||
quteproc.send_cmd(':yank selection')
|
||||
|
||||
expected_message = '{} chars yanked to clipboard'.format(len(input_text))
|
||||
quteproc.mark_expected(category='message',
|
||||
|
@ -291,6 +291,21 @@ class TestRegister:
|
||||
else:
|
||||
assert cmd._get_call_args(win_id=0) == ([expected], {})
|
||||
|
||||
def test_pos_arg_info(self):
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('foo', choices=('a', 'b'))
|
||||
@cmdutils.argument('bar', choices=('x', 'y'))
|
||||
@cmdutils.argument('opt')
|
||||
def fun(foo, bar, opt=False):
|
||||
"""Blah."""
|
||||
pass
|
||||
|
||||
cmd = cmdutils.cmd_dict['fun']
|
||||
assert cmd.get_pos_arg_info(0) == command.ArgInfo(choices=('a', 'b'))
|
||||
assert cmd.get_pos_arg_info(1) == command.ArgInfo(choices=('x', 'y'))
|
||||
with pytest.raises(IndexError):
|
||||
cmd.get_pos_arg_info(2)
|
||||
|
||||
|
||||
class TestArgument:
|
||||
|
||||
|
@ -27,6 +27,7 @@ from PyQt5.QtGui import QStandardItemModel
|
||||
|
||||
from qutebrowser.completion import completer
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.commands import command, cmdutils
|
||||
|
||||
|
||||
class FakeCompletionModel(QStandardItemModel):
|
||||
@ -91,24 +92,48 @@ def instances(monkeypatch):
|
||||
@pytest.fixture(autouse=True)
|
||||
def cmdutils_patch(monkeypatch, stubs):
|
||||
"""Patch the cmdutils module to provide fake commands."""
|
||||
@cmdutils.argument('section_', completion=usertypes.Completion.section)
|
||||
@cmdutils.argument('option', completion=usertypes.Completion.option)
|
||||
@cmdutils.argument('value', completion=usertypes.Completion.value)
|
||||
def set_command(section_=None, option=None, value=None):
|
||||
"""docstring!"""
|
||||
pass
|
||||
|
||||
@cmdutils.argument('topic', completion=usertypes.Completion.helptopic)
|
||||
def show_help(tab=False, bg=False, window=False, topic=None):
|
||||
"""docstring!"""
|
||||
pass
|
||||
|
||||
@cmdutils.argument('url', completion=usertypes.Completion.url)
|
||||
@cmdutils.argument('count', count=True)
|
||||
def openurl(url=None, implicit=False, bg=False, tab=False, window=False,
|
||||
count=None):
|
||||
"""docstring!"""
|
||||
pass
|
||||
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@cmdutils.argument('command', completion=usertypes.Completion.command)
|
||||
def bind(key, win_id, command=None, *, mode='normal', force=False):
|
||||
"""docstring!"""
|
||||
# pylint: disable=unused-variable
|
||||
pass
|
||||
|
||||
def tab_detach():
|
||||
"""docstring!"""
|
||||
pass
|
||||
|
||||
cmds = {
|
||||
'set': [usertypes.Completion.section, usertypes.Completion.option,
|
||||
usertypes.Completion.value],
|
||||
'help': [usertypes.Completion.helptopic],
|
||||
'quickmark-load': [usertypes.Completion.quickmark_by_name],
|
||||
'bookmark-load': [usertypes.Completion.bookmark_by_url],
|
||||
'open': [usertypes.Completion.url],
|
||||
'buffer': [usertypes.Completion.tab],
|
||||
'session-load': [usertypes.Completion.sessions],
|
||||
'bind': [usertypes.Completion.empty, usertypes.Completion.command],
|
||||
'tab-detach': None,
|
||||
'set': set_command,
|
||||
'help': show_help,
|
||||
'open': openurl,
|
||||
'bind': bind,
|
||||
'tab-detach': tab_detach,
|
||||
}
|
||||
cmd_utils = stubs.FakeCmdUtils({
|
||||
name: stubs.FakeCommand(completion=compl)
|
||||
for name, compl in cmds.items()
|
||||
name: command.Command(name=name, handler=fn)
|
||||
for name, fn in cmds.items()
|
||||
})
|
||||
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils',
|
||||
cmd_utils)
|
||||
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils)
|
||||
|
||||
|
||||
def _set_cmd_prompt(cmd, txt):
|
||||
@ -143,21 +168,17 @@ def _validate_cmd_prompt(cmd, txt):
|
||||
(':set general ignore-case |', usertypes.Completion.value),
|
||||
(':set general huh |', None),
|
||||
(':help |', usertypes.Completion.helptopic),
|
||||
(':quickmark-load |', usertypes.Completion.quickmark_by_name),
|
||||
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
|
||||
(':help |', usertypes.Completion.helptopic),
|
||||
(':open |', usertypes.Completion.url),
|
||||
(':buffer |', usertypes.Completion.tab),
|
||||
(':session-load |', usertypes.Completion.sessions),
|
||||
(':bind |', usertypes.Completion.empty),
|
||||
(':bind |', None),
|
||||
(':bind <c-x> |', usertypes.Completion.command),
|
||||
(':bind <c-x> foo|', usertypes.Completion.command),
|
||||
(':bind <c-x>| foo', usertypes.Completion.empty),
|
||||
(':bind <c-x>| foo', None),
|
||||
(':set| general ', usertypes.Completion.command),
|
||||
(':|set general ', usertypes.Completion.command),
|
||||
(':set gene|ral ignore-case', usertypes.Completion.section),
|
||||
(':|', usertypes.Completion.command),
|
||||
(': |', usertypes.Completion.command),
|
||||
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
|
||||
('/|', None),
|
||||
(':open -t|', None),
|
||||
(':open --tab|', None),
|
||||
|
@ -21,9 +21,44 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from qutebrowser.completion.models import base, sortfilter
|
||||
|
||||
|
||||
def _create_model(data):
|
||||
"""Create a completion model populated with the given data.
|
||||
|
||||
data: A list of lists, where each sub-list represents a category, each
|
||||
tuple in the sub-list represents an item, and each value in the
|
||||
tuple represents the item data for that column
|
||||
"""
|
||||
model = base.BaseCompletionModel()
|
||||
for catdata in data:
|
||||
cat = model.new_category('')
|
||||
for itemdata in catdata:
|
||||
model.new_item(cat, *itemdata)
|
||||
return model
|
||||
|
||||
|
||||
def _extract_model_data(model):
|
||||
"""Express a model's data as a list for easier comparison.
|
||||
|
||||
Return: A list of lists, where each sub-list represents a category, each
|
||||
tuple in the sub-list represents an item, and each value in the
|
||||
tuple represents the item data for that column
|
||||
"""
|
||||
data = []
|
||||
for i in range(0, model.rowCount()):
|
||||
cat_idx = model.index(i, 0)
|
||||
row = []
|
||||
for j in range(0, model.rowCount(cat_idx)):
|
||||
row.append((model.data(cat_idx.child(j, 0)),
|
||||
model.data(cat_idx.child(j, 1)),
|
||||
model.data(cat_idx.child(j, 2))))
|
||||
data.append(row)
|
||||
return data
|
||||
|
||||
@pytest.mark.parametrize('pattern, data, expected', [
|
||||
('foo', 'barfoobar', True),
|
||||
('foo', 'barFOObar', True),
|
||||
@ -46,3 +81,145 @@ def test_filter_accepts_row(pattern, data, expected):
|
||||
|
||||
row_count = filter_model.rowCount(idx)
|
||||
assert row_count == (1 if expected else 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, first, last', [
|
||||
([[('Aa',)]], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [('Ba',)]], 'Aa', 'Ba'),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]],
|
||||
'Aa', 'Ca'),
|
||||
([[], [('Ba',)]], 'Ba', 'Ba'),
|
||||
([[], [], [('Ca',)]], 'Ca', 'Ca'),
|
||||
([[], [], [('Ca',), ('Cb',)]], 'Ca', 'Cb'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], [('Ca',)]], 'Aa', 'Ca'),
|
||||
([[], []], None, None),
|
||||
])
|
||||
def test_first_last_item(tree, first, last):
|
||||
"""Test that first() and last() return indexes to the first and last items.
|
||||
|
||||
Args:
|
||||
tree: Each list represents a completion category, with each string
|
||||
being an item under that category.
|
||||
first: text of the first item
|
||||
last: text of the last item
|
||||
"""
|
||||
model = _create_model(tree)
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
assert filter_model.data(filter_model.first_item()) == first
|
||||
assert filter_model.data(filter_model.last_item()) == last
|
||||
|
||||
|
||||
def test_set_source_model():
|
||||
"""Ensure setSourceModel sets source_model and clears the pattern."""
|
||||
model1 = base.BaseCompletionModel()
|
||||
model2 = base.BaseCompletionModel()
|
||||
filter_model = sortfilter.CompletionFilterModel(model1)
|
||||
filter_model.set_pattern('foo')
|
||||
# sourceModel() is cached as srcmodel, so make sure both match
|
||||
assert filter_model.srcmodel is model1
|
||||
assert filter_model.sourceModel() is model1
|
||||
assert filter_model.pattern == 'foo'
|
||||
filter_model.setSourceModel(model2)
|
||||
assert filter_model.srcmodel is model2
|
||||
assert filter_model.sourceModel() is model2
|
||||
assert not filter_model.pattern
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, expected', [
|
||||
([[('Aa',)]], 1),
|
||||
([[('Aa',)], [('Ba',)]], 2),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]], 6),
|
||||
([[], [('Ba',)]], 1),
|
||||
([[], [], [('Ca',)]], 1),
|
||||
([[], [], [('Ca',), ('Cb',)]], 2),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], [], []], 1),
|
||||
([[('Aa',)], [], [('Ca',)]], 2),
|
||||
])
|
||||
def test_count(tree, expected):
|
||||
model = _create_model(tree)
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
assert filter_model.count() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, dumb_sort, filter_cols, before, after', [
|
||||
('foo', None, [0],
|
||||
[[('foo', '', ''), ('bar', '', '')]],
|
||||
[[('foo', '', '')]]),
|
||||
|
||||
('foo', None, [0],
|
||||
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
|
||||
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
|
||||
|
||||
('foo', None, [0],
|
||||
[[('foo', '', '')], [('bar', '', '')]],
|
||||
[[('foo', '', '')], []]),
|
||||
|
||||
# prefer foobar as it starts with the pattern
|
||||
('foo', None, [0],
|
||||
[[('barfoo', '', ''), ('foobar', '', '')]],
|
||||
[[('foobar', '', ''), ('barfoo', '', '')]]),
|
||||
|
||||
# however, don't rearrange categories
|
||||
('foo', None, [0],
|
||||
[[('barfoo', '', '')], [('foobar', '', '')]],
|
||||
[[('barfoo', '', '')], [('foobar', '', '')]]),
|
||||
|
||||
('foo', None, [1],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
|
||||
[[('bar', 'foo', '')]]),
|
||||
|
||||
('foo', None, [0, 1],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
|
||||
|
||||
('foo', None, [0, 1, 2],
|
||||
[[('foo', '', ''), ('bar', '')]],
|
||||
[[('foo', '', '')]]),
|
||||
|
||||
# the fourth column is the sort role, which overrides data-based sorting
|
||||
('', None, [0],
|
||||
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
|
||||
[[('one', '', ''), ('two', '', ''), ('three', '', '')]]),
|
||||
|
||||
('', Qt.AscendingOrder, [0],
|
||||
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
|
||||
[[('one', '', ''), ('two', '', ''), ('three', '', '')]]),
|
||||
|
||||
('', Qt.DescendingOrder, [0],
|
||||
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
|
||||
[[('three', '', ''), ('two', '', ''), ('one', '', '')]]),
|
||||
])
|
||||
def test_set_pattern(pattern, dumb_sort, filter_cols, before, after):
|
||||
"""Validate the filtering and sorting results of set_pattern."""
|
||||
model = _create_model(before)
|
||||
model.DUMB_SORT = dumb_sort
|
||||
model.columns_to_filter = filter_cols
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
filter_model.set_pattern(pattern)
|
||||
actual = _extract_model_data(filter_model)
|
||||
assert actual == after
|
||||
|
||||
|
||||
def test_sort():
|
||||
"""Ensure that a sort argument passed to sort overrides DUMB_SORT.
|
||||
|
||||
While test_set_pattern above covers most of the sorting logic, this
|
||||
particular case is easier to test separately.
|
||||
"""
|
||||
model = _create_model([[('B', '', '', 1),
|
||||
('C', '', '', 2),
|
||||
('A', '', '', 0)]])
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
|
||||
filter_model.sort(0, Qt.AscendingOrder)
|
||||
actual = _extract_model_data(filter_model)
|
||||
assert actual == [[('A', '', ''), ('B', '', ''), ('C', '', '')]]
|
||||
|
||||
filter_model.sort(0, Qt.DescendingOrder)
|
||||
actual = _extract_model_data(filter_model)
|
||||
assert actual == [[('C', '', ''), ('B', '', ''), ('A', '', '')]]
|
||||
|
@ -287,6 +287,17 @@ class TestKeyConfigParser:
|
||||
('hint links fill ":open -t {hint-url}"',
|
||||
'hint links fill :open -t {hint-url}'),
|
||||
|
||||
('yank-selected', 'yank selection'),
|
||||
('yank-selected --sel', 'yank selection --sel'),
|
||||
('yank-selected -p', 'yank selection -s'),
|
||||
|
||||
('yank -t', 'yank title'),
|
||||
('yank -ts', 'yank title -s'),
|
||||
('yank -d', 'yank domain'),
|
||||
('yank -ds', 'yank domain -s'),
|
||||
('yank -p', 'yank pretty-url'),
|
||||
('yank -ps', 'yank pretty-url -s'),
|
||||
|
||||
('paste', 'open {clipboard}'),
|
||||
('paste -t', 'open -t {clipboard}'),
|
||||
('paste -ws', 'open -w {primary}'),
|
||||
|
@ -69,14 +69,14 @@ class TestArg:
|
||||
config_stub.data['general']['editor'] = ['bin', 'foo', '{}', 'bar']
|
||||
editor.edit("")
|
||||
editor._proc._proc.start.assert_called_with(
|
||||
"bin", ["foo", editor._filename, "bar"])
|
||||
"bin", ["foo", editor._file.name, "bar"])
|
||||
|
||||
def test_placeholder_inline(self, config_stub, editor):
|
||||
"""Test starting editor with placeholder arg inside of another arg."""
|
||||
config_stub.data['general']['editor'] = ['bin', 'foo{}', 'bar']
|
||||
editor.edit("")
|
||||
editor._proc._proc.start.assert_called_with(
|
||||
"bin", ["foo" + editor._filename, "bar"])
|
||||
"bin", ["foo" + editor._file.name, "bar"])
|
||||
|
||||
|
||||
class TestFileHandling:
|
||||
@ -86,7 +86,7 @@ class TestFileHandling:
|
||||
def test_ok(self, editor):
|
||||
"""Test file handling when closing with an exit status == 0."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
assert os.path.basename(filename).startswith('qutebrowser-editor-')
|
||||
editor._proc.finished.emit(0, QProcess.NormalExit)
|
||||
@ -95,7 +95,7 @@ class TestFileHandling:
|
||||
def test_error(self, editor):
|
||||
"""Test file handling when closing with an exit status != 0."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
|
||||
editor._proc._proc.exitStatus = mock.Mock(
|
||||
@ -109,7 +109,7 @@ class TestFileHandling:
|
||||
def test_crash(self, editor):
|
||||
"""Test file handling when closing with a crash."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
|
||||
editor._proc._proc.exitStatus = mock.Mock(
|
||||
@ -125,7 +125,7 @@ class TestFileHandling:
|
||||
def test_unreadable(self, message_mock, editor):
|
||||
"""Test file handling when closing with an unreadable file."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
os.chmod(filename, 0o077)
|
||||
editor._proc.finished.emit(0, QProcess.NormalExit)
|
||||
@ -160,10 +160,10 @@ def test_modify(editor, initial_text, edited_text):
|
||||
"""Test if inputs get modified correctly."""
|
||||
editor.edit(initial_text)
|
||||
|
||||
with open(editor._filename, 'r', encoding='utf-8') as f:
|
||||
with open(editor._file.name, 'r', encoding='utf-8') as f:
|
||||
assert f.read() == initial_text
|
||||
|
||||
with open(editor._filename, 'w', encoding='utf-8') as f:
|
||||
with open(editor._file.name, 'w', encoding='utf-8') as f:
|
||||
f.write(edited_text)
|
||||
|
||||
editor._proc.finished.emit(0, QProcess.NormalExit)
|
||||
|
Loading…
Reference in New Issue
Block a user