Also make it possible to use multiple variables in one argument.
This commit is contained in:
Jan Verbeek 2016-08-07 13:14:46 +02:00
commit ebfe23c376
28 changed files with 467 additions and 235 deletions

View File

@ -44,6 +44,17 @@ Changed
(i.e. to open it at the position it would be opened if it was a clicked link) (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` - `:download-open` and `:prompt-open-download` now have an optional `cmdline`
argument to pass a commandline to open the download with. 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) v0.8.3 (unreleased)
------------------- -------------------
@ -52,6 +63,7 @@ Fixed
~~~~~ ~~~~~
- Fixed crash when doing `:<space><enter>`, another corner-case introduced in v0.8.0 - Fixed crash when doing `:<space><enter>`, another corner-case introduced in v0.8.0
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows
v0.8.2 v0.8.2
------ ------

View File

@ -146,13 +146,13 @@ Contributors, sorted by the number of commits in descending order:
* Lamar Pavel * Lamar Pavel
* Bruno Oliveira * Bruno Oliveira
* Alexander Cogneau * Alexander Cogneau
* Marshall Lochbaum
* Felix Van der Jeugt * Felix Van der Jeugt
* Jakub Klinkovský * Jakub Klinkovský
* Martin Tournoij * Martin Tournoij
* Marshall Lochbaum * Jan Verbeek
* Raphael Pierzina * Raphael Pierzina
* Joel Torstensson * Joel Torstensson
* Jan Verbeek
* Patric Schmitz * Patric Schmitz
* Tarcisio Fedrizzi * Tarcisio Fedrizzi
* Claude * Claude

View File

@ -65,8 +65,7 @@
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs). |<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|<<view-source,view-source>>|Show the source of the current page. |<<view-source,view-source>>|Show the source of the current page.
|<<wq,wq>>|Save open pages and quit. |<<wq,wq>>|Save open pages and quit.
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection. |<<yank,yank>>|Yank something to the clipboard or primary selection.
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|<<zoom,zoom>>|Set the zoom level for the current tab. |<<zoom,zoom>>|Set the zoom level for the current tab.
|<<zoom-in,zoom-in>>|Increase 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. |<<zoom-out,zoom-out>>|Decrease the zoom level for the current tab.
@ -110,6 +109,7 @@ Bind a key to a command.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * 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. * 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]]
=== bookmark-add === bookmark-add
@ -424,6 +424,7 @@ Execute a command after some time.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * 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. * With this command, +;;+ is interpreted literally instead of splitting off a second command.
* This command does not replace variables like +\{url\}+.
[[messages]] [[messages]]
=== messages === messages
@ -579,6 +580,7 @@ Repeat a given command.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * 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. * With this command, +;;+ is interpreted literally instead of splitting off a second command.
* This command does not replace variables like +\{url\}+.
[[report]] [[report]]
=== report === report
@ -840,25 +842,25 @@ Save open pages and quit.
[[yank]] [[yank]]
=== 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.
==== 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.
==== 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.
[[yank-selected]]
=== yank-selected
Syntax: +:yank-selected [*--sel*] [*--keep*]+
Yank the selected text to the clipboard or primary selection.
==== optional arguments ==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. * +*-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]]
=== zoom === zoom

View File

@ -1,2 +1,2 @@
pip==8.1.2 pip==8.1.2
setuptools==25.1.4 setuptools==25.1.6

View File

@ -3,4 +3,4 @@
pluggy==0.3.1 pluggy==0.3.1
py==1.4.31 py==1.4.31
tox==2.3.1 tox==2.3.1
virtualenv==15.0.2 virtualenv==15.0.3

View File

@ -645,30 +645,44 @@ class CommandDispatcher:
"representation.") "representation.")
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def yank(self, title=False, sel=False, domain=False, pretty=False): @cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
"""Yank the current URL/title to the clipboard or primary selection. 'title', 'domain'])
def yank(self, what='url', sel=False, keep=False):
"""Yank something to the clipboard or primary selection.
Args: 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. sel: Use the primary selection instead of the clipboard.
title: Yank the title instead of the URL. keep: Stay in visual mode after yanking the selection.
domain: Yank only the scheme, domain, and port number.
pretty: Yank the URL in pretty decoded form.
""" """
if title: if what == 'title':
s = self._tabbed_browser.page_title(self._current_index()) s = self._tabbed_browser.page_title(self._current_index())
what = 'title' elif what == 'domain':
elif domain:
port = self._current_url().port() port = self._current_url().port()
s = '{}://{}{}'.format(self._current_url().scheme(), s = '{}://{}{}'.format(self._current_url().scheme(),
self._current_url().host(), self._current_url().host(),
':' + str(port) if port > -1 else '') ':' + str(port) if port > -1 else '')
what = 'domain' elif what in ['url', 'pretty-url']:
else:
flags = QUrl.RemovePassword flags = QUrl.RemovePassword
if not pretty: if what != 'pretty-url':
flags |= QUrl.FullyEncoded flags |= QUrl.FullyEncoded
s = self._current_url().toString(flags) 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(): if sel and utils.supports_selection():
target = "primary selection" target = "primary selection"
@ -677,8 +691,15 @@ class CommandDispatcher:
target = "clipboard" target = "clipboard"
utils.set_clipboard(s, selection=sel) utils.set_clipboard(s, selection=sel)
if what != 'selection':
message.info(self._win_id, "Yanked {} to {}: {}".format( message.info(self._win_id, "Yanked {} to {}: {}".format(
what, target, s)) 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.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -982,7 +1003,7 @@ class CommandDispatcher:
self._tabbed_browser.setUpdatesEnabled(True) self._tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window', @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): def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
"""Spawn a command in a shell. """Spawn a command in a shell.
@ -1755,31 +1776,6 @@ class CommandDispatcher:
"""Move the cursor or selection to the end of the document.""" """Move the cursor or selection to the end of the document."""
self._current_widget().caret.move_to_end_of_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, @cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window') modes=[KeyMode.caret], scope='window')
def toggle_selection(self): def toggle_selection(self):

View File

@ -75,13 +75,13 @@ class Command:
deprecated: False, or a string to describe why a command is deprecated. deprecated: False, or a string to describe why a command is deprecated.
desc: The description of the command. desc: The description of the command.
handler: The handler function to call. 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). debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command. parser: The ArgumentParser to use to parse this command.
flags_with_args: A list of flags which take an argument. flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored. no_cmd_split: If true, ';;' to split sub-commands is ignored.
backend: Which backend the command works with (or None if it works with backend: Which backend the command works with (or None if it works with
both) both)
no_replace_variables: Don't replace variables like {url}
_qute_args: The saved data from @cmdutils.argument _qute_args: The saved data from @cmdutils.argument
_needs_js: Whether the command needs javascript enabled _needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in. _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, hide=False, modes=None, not_modes=None, needs_js=False,
debug=False, ignore_args=False, deprecated=False, debug=False, ignore_args=False, deprecated=False,
no_cmd_split=False, star_args_optional=False, scope='global', 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. # I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
if modes is not None and not_modes is not None: if modes is not None and not_modes is not None:
@ -127,6 +127,7 @@ class Command:
self.handler = handler self.handler = handler
self.no_cmd_split = no_cmd_split self.no_cmd_split = no_cmd_split
self.backend = backend self.backend = backend
self.no_replace_variables = no_replace_variables
self.docparser = docutils.DocstringParser(handler) self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser( self.parser = argparser.ArgumentParser(
@ -148,13 +149,7 @@ class Command:
self._qute_args = getattr(self.handler, 'qute_args', {}) self._qute_args = getattr(self.handler, 'qute_args', {})
self.handler.qute_args = None self.handler.qute_args = None
args = self._inspect_func() 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)
def _check_prerequisites(self, win_id): def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently. """Check if the command is permitted to run currently.
@ -208,6 +203,11 @@ class Command:
"""Get an ArgInfo tuple for the given inspect.Parameter.""" """Get an ArgInfo tuple for the given inspect.Parameter."""
return self._qute_args.get(param.name, ArgInfo()) 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): def _inspect_special_param(self, param):
"""Check if the given parameter is a special one. """Check if the given parameter is a special one.

View File

@ -52,28 +52,27 @@ def replace_variables(win_id, arglist):
args = [] args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
if '{url}' in arglist: if any('{url}' in arg for arg in arglist):
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded | url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded |
QUrl.RemovePassword) 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) pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword)
try: try:
if '{clipboard}' in arglist: if any('{clipboard}' in arg for arg in arglist):
clipboard = utils.get_clipboard() clipboard = utils.get_clipboard()
if '{primary}' in arglist: if any('{primary}' in arg for arg in arglist):
primary = utils.get_clipboard(selection=True) primary = utils.get_clipboard(selection=True)
except utils.ClipboardEmptyError as e: except utils.ClipboardEmptyError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
for arg in arglist: for arg in arglist:
if arg == '{url}': if '{url}' in arg:
args.append(url) arg = arg.replace('{url}', url)
elif arg == '{url:pretty}': if '{url:pretty}' in arg:
args.append(pretty_url) arg = arg.replace('{url:pretty}', pretty_url)
elif arg == '{clipboard}': if '{clipboard}' in arg:
args.append(clipboard) arg = arg.replace('{clipboard}', clipboard)
elif arg == '{primary}': if '{primary}' in arg:
args.append(primary) arg = arg.replace('{primary}', primary)
else:
args.append(arg) args.append(arg)
return args return args
@ -290,6 +289,9 @@ class CommandRunner(QObject):
window=self._win_id) window=self._win_id)
cur_mode = mode_manager.mode cur_mode = mode_manager.mode
if result.cmd.no_replace_variables:
args = result.args
else:
args = replace_variables(self._win_id, result.args) args = replace_variables(self._win_id, result.args)
if count is not None: if count is not None:
if result.count is not None: if result.count is not None:

View File

@ -204,25 +204,18 @@ class Completer(QObject):
return sortfilter.CompletionFilterModel(source=model, parent=self) return sortfilter.CompletionFilterModel(source=model, parent=self)
# delegate completion to command # delegate completion to command
try: try:
completions = cmdutils.cmd_dict[parts[0]].completion cmd = cmdutils.cmd_dict[parts[0]]
except KeyError: except KeyError:
# entering an unknown command # entering an unknown command
return None return None
if completions is None:
# command without any available completions
return None
dbg_completions = [c.name for c in completions]
try: try:
idx = cursor_part - 1 idx = cursor_part - 1
completion = completions[idx] completion = cmd.get_pos_arg_info(idx).completion
except IndexError: except IndexError:
# More arguments than completions # user provided more positional arguments than the command takes
log.completion.debug("completions: {}".format( return None
', '.join(dbg_completions))) if completion is None:
return 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) model = self._get_completion_model(completion, parts, cursor_part)
return model return model

View File

@ -27,8 +27,7 @@ Module attributes:
import functools import functools
from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel, from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
base)
from qutebrowser.utils import objreg, usertypes, log, debug from qutebrowser.utils import objreg, usertypes, log, debug
from qutebrowser.config import configdata from qutebrowser.config import configdata
@ -115,13 +114,6 @@ def init_session_completion():
_instances[usertypes.Completion.sessions] = model _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 = { INITIALIZERS = {
usertypes.Completion.command: _init_command_completion, usertypes.Completion.command: _init_command_completion,
usertypes.Completion.helptopic: _init_helptopic_completion, usertypes.Completion.helptopic: _init_helptopic_completion,
@ -133,7 +125,6 @@ INITIALIZERS = {
usertypes.Completion.quickmark_by_name: init_quickmark_completions, usertypes.Completion.quickmark_by_name: init_quickmark_completions,
usertypes.Completion.bookmark_by_url: init_bookmark_completions, usertypes.Completion.bookmark_by_url: init_bookmark_completions,
usertypes.Completion.sessions: init_session_completion, usertypes.Completion.sessions: init_session_completion,
usertypes.Completion.empty: _init_empty_completion,
} }

View File

@ -135,8 +135,8 @@ class CompletionFilterModel(QSortFilterProxyModel):
for col in self.srcmodel.columns_to_filter: for col in self.srcmodel.columns_to_filter:
idx = self.srcmodel.index(row, col, parent) idx = self.srcmodel.index(row, col, parent)
if not idx.isValid(): if not idx.isValid(): # pragma: no cover
# No entries in parent model # this is a sanity check not hit by any test case
continue continue
data = self.srcmodel.data(idx) data = self.srcmodel.data(idx)
if not data: if not data:

View File

@ -1506,12 +1506,12 @@ KEY_DATA = collections.OrderedDict([
('enter-mode jump_mark', ["'"]), ('enter-mode jump_mark', ["'"]),
('yank', ['yy']), ('yank', ['yy']),
('yank -s', ['yY']), ('yank -s', ['yY']),
('yank -t', ['yt']), ('yank title', ['yt']),
('yank -ts', ['yT']), ('yank title -s', ['yT']),
('yank -d', ['yd']), ('yank domain', ['yd']),
('yank -ds', ['yD']), ('yank domain -s', ['yD']),
('yank -p', ['yp']), ('yank pretty-url', ['yp']),
('yank -ps', ['yP']), ('yank pretty-url -s', ['yP']),
('open {clipboard}', ['pp']), ('open {clipboard}', ['pp']),
('open {primary}', ['pP']), ('open {primary}', ['pP']),
('open -t {clipboard}', ['Pp']), ('open -t {clipboard}', ['Pp']),
@ -1638,8 +1638,8 @@ KEY_DATA = collections.OrderedDict([
('move-to-end-of-line', ['$']), ('move-to-end-of-line', ['$']),
('move-to-start-of-document', ['gg']), ('move-to-start-of-document', ['gg']),
('move-to-end-of-document', ['G']), ('move-to-end-of-document', ['G']),
('yank-selected -p', ['Y']), ('yank selection -s', ['Y']),
('yank-selected', ['y'] + RETURN_KEYS), ('yank selection', ['y'] + RETURN_KEYS),
('scroll left', ['H']), ('scroll left', ['H']),
('scroll down', ['J']), ('scroll down', ['J']),
('scroll up', ['K']), ('scroll up', ['K']),
@ -1678,6 +1678,15 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^hint links fill "([^"]*)"$'), r'hint links fill \1'), (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$'), r'open {clipboard}'),
(re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'), (re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'),
(re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'), (re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'),

View File

@ -150,9 +150,9 @@ class KeyConfigParser(QObject):
data = str(self) data = str(self)
f.write(data) 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('win_id', win_id=True)
@cmdutils.argument('key', completion=usertypes.Completion.empty)
@cmdutils.argument('command', completion=usertypes.Completion.command) @cmdutils.argument('command', completion=usertypes.Completion.command)
def bind(self, key, win_id, command=None, *, mode='normal', force=False): def bind(self, key, win_id, command=None, *, mode='normal', force=False):
"""Bind a key to a command. """Bind a key to a command.
@ -361,12 +361,12 @@ class KeyConfigParser(QObject):
raise KeyConfigError("Got command '{}' without getting a " raise KeyConfigError("Got command '{}' without getting a "
"section!".format(line)) "section!".format(line))
else: else:
self._validate_command(line)
for rgx, repl in configdata.CHANGED_KEY_COMMANDS: for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
if rgx.match(line): if rgx.match(line):
line = rgx.sub(repl, line) line = rgx.sub(repl, line)
self._mark_config_dirty() self._mark_config_dirty()
break break
self._validate_command(line)
self._cur_command = line self._cur_command = line
def _read_keybinding(self, line): def _read_keybinding(self, line):

View File

@ -23,8 +23,8 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtWidgets import QSizePolicy
from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.commands import cmdexc, cmdutils, runners from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.misc import cmdhistory, split from qutebrowser.misc import cmdhistory
from qutebrowser.misc import miscwidgets as misc from qutebrowser.misc import miscwidgets as misc
from qutebrowser.utils import usertypes, log, objreg 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. space: If given, a space is added to the end.
append: If given, the text is appended to the current text. 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: if space:
text += ' ' text += ' '
if append: if append:

View File

@ -35,8 +35,8 @@ class ExternalEditor(QObject):
Attributes: Attributes:
_text: The current text before the editor is opened. _text: The current text before the editor is opened.
_oshandle: The OS level handle to the tmpfile. _file: The file handle as tempfile.NamedTemporaryFile. Note that this
_filehandle: The file handle to the tmpfile. handle will be closed after the initial file has been created.
_proc: The GUIProcess of the editor. _proc: The GUIProcess of the editor.
_win_id: The window ID the ExternalEditor is associated with. _win_id: The window ID the ExternalEditor is associated with.
""" """
@ -46,20 +46,18 @@ class ExternalEditor(QObject):
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._text = None self._text = None
self._oshandle = None self._file = None
self._filename = None
self._proc = None self._proc = None
self._win_id = win_id self._win_id = win_id
def _cleanup(self): def _cleanup(self):
"""Clean up temporary files after the editor closed.""" """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. # Could not create initial file.
return return
try: try:
os.close(self._oshandle)
if self._proc.exit_status() != QProcess.CrashExit: if self._proc.exit_status() != QProcess.CrashExit:
os.remove(self._filename) os.remove(self._file.name)
except OSError as e: except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
@ -82,7 +80,7 @@ class ExternalEditor(QObject):
return return
encoding = config.get('general', 'editor-encoding') encoding = config.get('general', 'editor-encoding')
try: try:
with open(self._filename, 'r', encoding=encoding) as f: with open(self._file.name, 'r', encoding=encoding) as f:
text = f.read() text = f.read()
except OSError as e: except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
@ -108,13 +106,18 @@ class ExternalEditor(QObject):
if self._text is not None: if self._text is not None:
raise ValueError("Already editing a file!") raise ValueError("Already editing a file!")
self._text = text self._text = text
try:
self._oshandle, self._filename = tempfile.mkstemp(
text=True, prefix='qutebrowser-editor-')
if text:
encoding = config.get('general', 'editor-encoding') encoding = config.get('general', 'editor-encoding')
with open(self._filename, 'w', encoding=encoding) as f: try:
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: except OSError as e:
message.error(self._win_id, "Failed to create initial file: " message.error(self._win_id, "Failed to create initial file: "
"{}".format(e)) "{}".format(e))
@ -125,6 +128,6 @@ class ExternalEditor(QObject):
self._proc.error.connect(self.on_proc_error) self._proc.error.connect(self.on_proc_error)
editor = config.get('general', 'editor') editor = config.get('general', 'editor')
executable = editor[0] 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)) log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
self._proc.start(executable, args) self._proc.start(executable, args)

View File

@ -39,7 +39,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import 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) @cmdutils.argument('win_id', win_id=True)
def later(ms: int, command, win_id): def later(ms: int, command, win_id):
"""Execute a command after some time. """Execute a command after some time.
@ -69,7 +69,7 @@ def later(ms: int, command, win_id):
raise 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) @cmdutils.argument('win_id', win_id=True)
def repeat(times: int, command, win_id): def repeat(times: int, command, win_id):
"""Repeat a given command. """Repeat a given command.

View File

@ -238,8 +238,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
# Available command completions # Available command completions
Completion = enum('Completion', ['command', 'section', 'option', 'value', Completion = enum('Completion', ['command', 'section', 'option', 'value',
'helptopic', 'quickmark_by_name', 'helptopic', 'quickmark_by_name',
'bookmark_by_url', 'url', 'tab', 'sessions', 'bookmark_by_url', 'url', 'tab', 'sessions'])
'empty'])
# Exit statuses for errors. Needs to be an int for sys.exit. # Exit statuses for errors. Needs to be an int for sys.exit.

View File

@ -150,12 +150,14 @@ PERFECT_FILES = [
('tests/unit/completion/test_models.py', ('tests/unit/completion/test_models.py',
'qutebrowser/completion/models/base.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. # 100% coverage because of end2end tests, but no perfect unit tests yet.
WHITELISTED_FILES = [] WHITELISTED_FILES = ['qutebrowser/browser/webkit/webkitinspector.py']
class Skipped(Exception): class Skipped(Exception):

View File

@ -239,7 +239,8 @@ def _get_command_doc_notes(cmd):
Yield: Yield:
Strings which should be added to the docs. 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 ""
yield "==== note" yield "==== note"
if cmd.maxsplit is not None: if cmd.maxsplit is not None:
@ -248,6 +249,8 @@ def _get_command_doc_notes(cmd):
if cmd.no_cmd_split: if cmd.no_cmd_split:
yield ("* With this command, +;;+ is interpreted literally " yield ("* With this command, +;;+ is interpreted literally "
"instead of splitting off a second command.") "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): def _get_action_metavar(action, nargs=1):

View File

@ -10,7 +10,7 @@ Feature: Caret mode
Scenario: Selecting the entire document Scenario: Selecting the entire document
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-end-of-document And I run :move-to-end-of-document
And I run :yank-selected And I run :yank selection
Then the clipboard should contain: Then the clipboard should contain:
one two three one two three
eins zwei drei eins zwei drei
@ -23,14 +23,14 @@ Feature: Caret mode
And I run :move-to-start-of-document And I run :move-to-start-of-document
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "one" Then the clipboard should contain "one"
Scenario: Moving to end and to start of document (with selection) Scenario: Moving to end and to start of document (with selection)
When I run :move-to-end-of-document When I run :move-to-end-of-document
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-start-of-document And I run :move-to-start-of-document
And I run :yank-selected And I run :yank selection
Then the clipboard should contain: Then the clipboard should contain:
one two three one two three
eins zwei drei eins zwei drei
@ -43,7 +43,7 @@ Feature: Caret mode
Scenario: Selecting a block Scenario: Selecting a block
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-end-of-next-block And I run :move-to-end-of-next-block
And I run :yank-selected And I run :yank selection
Then the clipboard should contain: Then the clipboard should contain:
one two three one two three
eins zwei drei eins zwei drei
@ -53,7 +53,7 @@ Feature: Caret mode
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-prev-block And I run :move-to-end-of-prev-block
And I run :move-to-prev-word And I run :move-to-prev-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain: Then the clipboard should contain:
drei drei
@ -64,14 +64,14 @@ Feature: Caret mode
And I run :move-to-end-of-prev-block And I run :move-to-end-of-prev-block
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-prev-word And I run :move-to-prev-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "drei" Then the clipboard should contain "drei"
Scenario: Moving back to the start of previous block (with selection) Scenario: Moving back to the start of previous block (with selection)
When I run :move-to-end-of-next-block with count 2 When I run :move-to-end-of-next-block with count 2
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-start-of-prev-block And I run :move-to-start-of-prev-block
And I run :yank-selected And I run :yank selection
Then the clipboard should contain: Then the clipboard should contain:
eins zwei drei eins zwei drei
@ -82,20 +82,20 @@ Feature: Caret mode
And I run :move-to-start-of-prev-block And I run :move-to-start-of-prev-block
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-next-word And I run :move-to-next-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "eins " Then the clipboard should contain "eins "
Scenario: Moving to the start of next block (with selection) Scenario: Moving to the start of next block (with selection)
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-start-of-next-block 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" Then the clipboard should contain "one two three\n"
Scenario: Moving to the start of next block Scenario: Moving to the start of next block
When I run :move-to-start-of-next-block When I run :move-to-start-of-next-block
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "eins" Then the clipboard should contain "eins"
# line # line
@ -103,20 +103,20 @@ Feature: Caret mode
Scenario: Selecting a line Scenario: Selecting a line
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-end-of-line 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" Then the clipboard should contain "one two three"
Scenario: Moving and selecting a line Scenario: Moving and selecting a line
When I run :move-to-next-line When I run :move-to-next-line
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-line 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" Then the clipboard should contain "eins zwei drei"
Scenario: Selecting next line Scenario: Selecting next line
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-next-line 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" Then the clipboard should contain "one two three\n"
Scenario: Moving to end and to start of line 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 :move-to-start-of-line
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "one" Then the clipboard should contain "one"
Scenario: Selecting a line (backwards) Scenario: Selecting a line (backwards)
When I run :move-to-end-of-line When I run :move-to-end-of-line
And I run :toggle-selection And I run :toggle-selection
When I run :move-to-start-of-line 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" Then the clipboard should contain "one two three"
Scenario: Selecting previous line Scenario: Selecting previous line
When I run :move-to-next-line When I run :move-to-next-line
And I run :toggle-selection And I run :toggle-selection
When I run :move-to-prev-line 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" Then the clipboard should contain "one two three\n"
Scenario: Moving to previous line Scenario: Moving to previous line
@ -146,7 +146,7 @@ Feature: Caret mode
When I run :move-to-prev-line When I run :move-to-prev-line
And I run :toggle-selection And I run :toggle-selection
When I run :move-to-next-line 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" Then the clipboard should contain "one two three\n"
# word # word
@ -154,35 +154,35 @@ Feature: Caret mode
Scenario: Selecting a word Scenario: Selecting a word
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "one" Then the clipboard should contain "one"
Scenario: Moving to end and selecting a word Scenario: Moving to end and selecting a word
When I run :move-to-end-of-word When I run :move-to-end-of-word
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain " two" Then the clipboard should contain " two"
Scenario: Moving to next word and selecting a word Scenario: Moving to next word and selecting a word
When I run :move-to-next-word When I run :move-to-next-word
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "two" Then the clipboard should contain "two"
Scenario: Moving to next word and selecting until next word Scenario: Moving to next word and selecting until next word
When I run :move-to-next-word When I run :move-to-next-word
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-next-word And I run :move-to-next-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "two " Then the clipboard should contain "two "
Scenario: Moving to previous word and selecting a word Scenario: Moving to previous word and selecting a word
When I run :move-to-end-of-word When I run :move-to-end-of-word
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-prev-word And I run :move-to-prev-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "one" Then the clipboard should contain "one"
Scenario: Moving to previous word Scenario: Moving to previous word
@ -190,7 +190,7 @@ Feature: Caret mode
And I run :move-to-prev-word And I run :move-to-prev-word
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "one" Then the clipboard should contain "one"
# char # char
@ -198,21 +198,21 @@ Feature: Caret mode
Scenario: Selecting a char Scenario: Selecting a char
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-next-char And I run :move-to-next-char
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "o" Then the clipboard should contain "o"
Scenario: Moving and selecting a char Scenario: Moving and selecting a char
When I run :move-to-next-char When I run :move-to-next-char
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-next-char And I run :move-to-next-char
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "n" Then the clipboard should contain "n"
Scenario: Selecting previous char Scenario: Selecting previous char
When I run :move-to-end-of-word When I run :move-to-end-of-word
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-prev-char And I run :move-to-prev-char
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "e" Then the clipboard should contain "e"
Scenario: Moving to previous char Scenario: Moving to previous char
@ -220,41 +220,41 @@ Feature: Caret mode
And I run :move-to-prev-char And I run :move-to-prev-char
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "e" Then the clipboard should contain "e"
# :yank-selected # :yank selection
Scenario: :yank-selected without selection Scenario: :yank selection without selection
When I run :yank-selected When I run :yank selection
Then the message "Nothing to yank" should be shown. Then the message "Nothing to yank" should be shown.
Scenario: :yank-selected message Scenario: :yank selection message
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-end-of-word 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. 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 When I run :toggle-selection
And I run :move-to-next-char 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. 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 When selection is supported
And I run :toggle-selection And I run :toggle-selection
And I run :move-to-end-of-word 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. Then the message "3 chars yanked to primary selection" should be shown.
And the primary selection should contain "one" And the primary selection should contain "one"
Scenario: :yank-selected with --keep Scenario: :yank selection with --keep
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-end-of-word 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 :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. Then the message "3 chars yanked to clipboard" should be shown.
And the message "7 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" And the clipboard should contain "one two"
@ -265,7 +265,7 @@ Feature: Caret mode
When I run :toggle-selection When I run :toggle-selection
And I run :move-to-end-of-word And I run :move-to-end-of-word
And I run :drop-selection And I run :drop-selection
And I run :yank-selected And I run :yank selection
Then the message "Nothing to yank" should be shown. Then the message "Nothing to yank" should be shown.
# :follow-selected # :follow-selected

View File

@ -9,19 +9,19 @@ Feature: Searching on a page
Scenario: Searching text Scenario: Searching text
When I run :search foo When I run :search foo
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "foo" Then the clipboard should contain "foo"
Scenario: Searching twice Scenario: Searching twice
When I run :search foo When I run :search foo
And I run :search bar And I run :search bar
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "Bar" Then the clipboard should contain "Bar"
Scenario: Searching with --reverse Scenario: Searching with --reverse
When I set general -> ignore-case to true When I set general -> ignore-case to true
And I run :search -r foo And I run :search -r foo
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "Foo" Then the clipboard should contain "Foo"
Scenario: Searching without matches Scenario: Searching without matches
@ -32,13 +32,13 @@ Feature: Searching on a page
Scenario: Searching with / and spaces at the end (issue 874) Scenario: Searching with / and spaces at the end (issue 874)
When I run :set-cmd-text -s /space When I run :set-cmd-text -s /space
And I run :command-accept And I run :command-accept
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "space " Then the clipboard should contain "space "
Scenario: Searching with / and slash in search term (issue 507) Scenario: Searching with / and slash in search term (issue 507)
When I run :set-cmd-text -s //slash When I run :set-cmd-text -s //slash
And I run :command-accept And I run :command-accept
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "/slash" Then the clipboard should contain "/slash"
# This doesn't work because this is QtWebKit behavior. # 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 Scenario: Searching text with ignore-case = true
When I set general -> ignore-case to true When I set general -> ignore-case to true
And I run :search bar And I run :search bar
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "Bar" Then the clipboard should contain "Bar"
Scenario: Searching text with ignore-case = false Scenario: Searching text with ignore-case = false
When I set general -> ignore-case to false When I set general -> ignore-case to false
And I run :search bar And I run :search bar
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "bar" Then the clipboard should contain "bar"
Scenario: Searching text with ignore-case = smart (lower-case) Scenario: Searching text with ignore-case = smart (lower-case)
When I set general -> ignore-case to smart When I set general -> ignore-case to smart
And I run :search bar And I run :search bar
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "Bar" Then the clipboard should contain "Bar"
Scenario: Searching text with ignore-case = smart (upper-case) Scenario: Searching text with ignore-case = smart (upper-case)
When I set general -> ignore-case to smart When I set general -> ignore-case to smart
And I run :search Foo 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 Then the clipboard should contain "Foo" # even though foo was first
## :search-next ## :search-next
@ -79,21 +79,21 @@ Feature: Searching on a page
When I set general -> ignore-case to true When I set general -> ignore-case to true
And I run :search foo And 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" Then the clipboard should contain "Foo"
Scenario: Jumping to next match with count Scenario: Jumping to next match with count
When I set general -> ignore-case to true When I set general -> ignore-case to true
And I run :search baz And I run :search baz
And I run :search-next with count 2 And I run :search-next with count 2
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "BAZ" Then the clipboard should contain "BAZ"
Scenario: Jumping to next match with --reverse Scenario: Jumping to next match with --reverse
When I set general -> ignore-case to true When I set general -> ignore-case to true
And I run :search --reverse foo And 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" Then the clipboard should contain "foo"
Scenario: Jumping to next match without search Scenario: Jumping to next match without search
@ -107,7 +107,7 @@ Feature: Searching on a page
And I run :search foo And I run :search foo
And I run :tab-prev And I run :tab-prev
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" Then the clipboard should contain "foo"
## :search-prev ## :search-prev
@ -117,7 +117,7 @@ Feature: Searching on a page
And I run :search foo And I run :search foo
And I run :search-next And I run :search-next
And I run :search-prev And I run :search-prev
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "foo" Then the clipboard should contain "foo"
Scenario: Jumping to previous match with count 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-next And I run :search-next
And I run :search-prev with count 2 And I run :search-prev with count 2
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "baz" Then the clipboard should contain "baz"
Scenario: Jumping to previous match with --reverse 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 --reverse foo
And I run :search-next And I run :search-next
And I run :search-prev And I run :search-prev
And I run :yank-selected And I run :yank selection
Then the clipboard should contain "Foo" Then the clipboard should contain "Foo"
Scenario: Jumping to previous match without search Scenario: Jumping to previous match without search
@ -149,14 +149,14 @@ Feature: Searching on a page
When I run :search foo When I run :search foo
And I run :search-next And I run :search-next
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" Then the clipboard should contain "foo"
Scenario: Wrapping around page with --reverse Scenario: Wrapping around page with --reverse
When I run :search --reverse foo When I run :search --reverse foo
And I run :search-next And I run :search-next
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" Then the clipboard should contain "Foo"
# TODO: wrapping message with scrolling # TODO: wrapping message with scrolling

View File

@ -23,13 +23,13 @@ Feature: Yanking and pasting.
Scenario: Yanking title to clipboard Scenario: Yanking title to clipboard
When I open data/title.html When I open data/title.html
And I wait for regex "Changing title for idx \d to 'Test title'" in the log 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 Then the message "Yanked title to clipboard: Test title" should be shown
And the clipboard should contain "Test title" And the clipboard should contain "Test title"
Scenario: Yanking domain to clipboard Scenario: Yanking domain to clipboard
When I open data/title.html 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 Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown
And the clipboard should contain "http://localhost:(port)" And the clipboard should contain "http://localhost:(port)"
@ -41,7 +41,7 @@ Feature: Yanking and pasting.
Scenario: Yanking pretty decoded URL Scenario: Yanking pretty decoded URL
When I open data/title with spaces.html 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 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" And the clipboard should contain "http://localhost:(port)/data/title with spaces.html"

View File

@ -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(':enter-mode caret')
quteproc.send_cmd(':toggle-selection') quteproc.send_cmd(':toggle-selection')
quteproc.send_cmd(':move-to-prev-word') 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)) expected_message = '{} chars yanked to clipboard'.format(len(input_text))
quteproc.mark_expected(category='message', quteproc.mark_expected(category='message',

View File

@ -291,6 +291,21 @@ class TestRegister:
else: else:
assert cmd._get_call_args(win_id=0) == ([expected], {}) 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: class TestArgument:

View File

@ -27,6 +27,7 @@ from PyQt5.QtGui import QStandardItemModel
from qutebrowser.completion import completer from qutebrowser.completion import completer
from qutebrowser.utils import usertypes from qutebrowser.utils import usertypes
from qutebrowser.commands import command, cmdutils
class FakeCompletionModel(QStandardItemModel): class FakeCompletionModel(QStandardItemModel):
@ -91,24 +92,48 @@ def instances(monkeypatch):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def cmdutils_patch(monkeypatch, stubs): def cmdutils_patch(monkeypatch, stubs):
"""Patch the cmdutils module to provide fake commands.""" """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 = { cmds = {
'set': [usertypes.Completion.section, usertypes.Completion.option, 'set': set_command,
usertypes.Completion.value], 'help': show_help,
'help': [usertypes.Completion.helptopic], 'open': openurl,
'quickmark-load': [usertypes.Completion.quickmark_by_name], 'bind': bind,
'bookmark-load': [usertypes.Completion.bookmark_by_url], 'tab-detach': tab_detach,
'open': [usertypes.Completion.url],
'buffer': [usertypes.Completion.tab],
'session-load': [usertypes.Completion.sessions],
'bind': [usertypes.Completion.empty, usertypes.Completion.command],
'tab-detach': None,
} }
cmd_utils = stubs.FakeCmdUtils({ cmd_utils = stubs.FakeCmdUtils({
name: stubs.FakeCommand(completion=compl) name: command.Command(name=name, handler=fn)
for name, compl in cmds.items() for name, fn in cmds.items()
}) })
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils)
cmd_utils)
def _set_cmd_prompt(cmd, txt): 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 ignore-case |', usertypes.Completion.value),
(':set general huh |', None), (':set general huh |', None),
(':help |', usertypes.Completion.helptopic), (':help |', usertypes.Completion.helptopic),
(':quickmark-load |', usertypes.Completion.quickmark_by_name), (':help |', usertypes.Completion.helptopic),
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
(':open |', usertypes.Completion.url), (':open |', usertypes.Completion.url),
(':buffer |', usertypes.Completion.tab), (':bind |', None),
(':session-load |', usertypes.Completion.sessions),
(':bind |', usertypes.Completion.empty),
(':bind <c-x> |', usertypes.Completion.command), (':bind <c-x> |', usertypes.Completion.command),
(':bind <c-x> foo|', 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 general ', usertypes.Completion.command), (':|set general ', usertypes.Completion.command),
(':set gene|ral ignore-case', usertypes.Completion.section), (':set gene|ral ignore-case', usertypes.Completion.section),
(':|', usertypes.Completion.command), (':|', usertypes.Completion.command),
(': |', usertypes.Completion.command), (': |', usertypes.Completion.command),
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
('/|', None), ('/|', None),
(':open -t|', None), (':open -t|', None),
(':open --tab|', None), (':open --tab|', None),

View File

@ -21,9 +21,44 @@
import pytest import pytest
from PyQt5.QtCore import Qt
from qutebrowser.completion.models import base, sortfilter 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', [ @pytest.mark.parametrize('pattern, data, expected', [
('foo', 'barfoobar', True), ('foo', 'barfoobar', True),
('foo', 'barFOObar', True), ('foo', 'barFOObar', True),
@ -46,3 +81,145 @@ def test_filter_accepts_row(pattern, data, expected):
row_count = filter_model.rowCount(idx) row_count = filter_model.rowCount(idx)
assert row_count == (1 if expected else 0) 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', '', '')]]

View File

@ -287,6 +287,17 @@ class TestKeyConfigParser:
('hint links fill ":open -t {hint-url}"', ('hint links fill ":open -t {hint-url}"',
'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', 'open {clipboard}'),
('paste -t', 'open -t {clipboard}'), ('paste -t', 'open -t {clipboard}'),
('paste -ws', 'open -w {primary}'), ('paste -ws', 'open -w {primary}'),

View File

@ -69,14 +69,14 @@ class TestArg:
config_stub.data['general']['editor'] = ['bin', 'foo', '{}', 'bar'] config_stub.data['general']['editor'] = ['bin', 'foo', '{}', 'bar']
editor.edit("") editor.edit("")
editor._proc._proc.start.assert_called_with( 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): def test_placeholder_inline(self, config_stub, editor):
"""Test starting editor with placeholder arg inside of another arg.""" """Test starting editor with placeholder arg inside of another arg."""
config_stub.data['general']['editor'] = ['bin', 'foo{}', 'bar'] config_stub.data['general']['editor'] = ['bin', 'foo{}', 'bar']
editor.edit("") editor.edit("")
editor._proc._proc.start.assert_called_with( editor._proc._proc.start.assert_called_with(
"bin", ["foo" + editor._filename, "bar"]) "bin", ["foo" + editor._file.name, "bar"])
class TestFileHandling: class TestFileHandling:
@ -86,7 +86,7 @@ class TestFileHandling:
def test_ok(self, editor): def test_ok(self, editor):
"""Test file handling when closing with an exit status == 0.""" """Test file handling when closing with an exit status == 0."""
editor.edit("") editor.edit("")
filename = editor._filename filename = editor._file.name
assert os.path.exists(filename) assert os.path.exists(filename)
assert os.path.basename(filename).startswith('qutebrowser-editor-') assert os.path.basename(filename).startswith('qutebrowser-editor-')
editor._proc.finished.emit(0, QProcess.NormalExit) editor._proc.finished.emit(0, QProcess.NormalExit)
@ -95,7 +95,7 @@ class TestFileHandling:
def test_error(self, editor): def test_error(self, editor):
"""Test file handling when closing with an exit status != 0.""" """Test file handling when closing with an exit status != 0."""
editor.edit("") editor.edit("")
filename = editor._filename filename = editor._file.name
assert os.path.exists(filename) assert os.path.exists(filename)
editor._proc._proc.exitStatus = mock.Mock( editor._proc._proc.exitStatus = mock.Mock(
@ -109,7 +109,7 @@ class TestFileHandling:
def test_crash(self, editor): def test_crash(self, editor):
"""Test file handling when closing with a crash.""" """Test file handling when closing with a crash."""
editor.edit("") editor.edit("")
filename = editor._filename filename = editor._file.name
assert os.path.exists(filename) assert os.path.exists(filename)
editor._proc._proc.exitStatus = mock.Mock( editor._proc._proc.exitStatus = mock.Mock(
@ -125,7 +125,7 @@ class TestFileHandling:
def test_unreadable(self, message_mock, editor): def test_unreadable(self, message_mock, editor):
"""Test file handling when closing with an unreadable file.""" """Test file handling when closing with an unreadable file."""
editor.edit("") editor.edit("")
filename = editor._filename filename = editor._file.name
assert os.path.exists(filename) assert os.path.exists(filename)
os.chmod(filename, 0o077) os.chmod(filename, 0o077)
editor._proc.finished.emit(0, QProcess.NormalExit) 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.""" """Test if inputs get modified correctly."""
editor.edit(initial_text) 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 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) f.write(edited_text)
editor._proc.finished.emit(0, QProcess.NormalExit) editor._proc.finished.emit(0, QProcess.NormalExit)