Merge branch 'histcomplete'

This commit is contained in:
Florian Bruhin 2015-03-17 06:16:26 +01:00
commit 94bc10405a
21 changed files with 591 additions and 224 deletions

View File

@ -136,6 +136,7 @@ Contributors, sorted by the number of commits in descending order:
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* Jimmy
* rikn00
* Patric Schmitz
* Martin Zimmermann

View File

@ -60,9 +60,11 @@
|==============
|Setting|Description
|<<completion-download-path-suggestion,download-path-suggestion>>|What to display in the download filename input.
|<<completion-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history)
|<<completion-show,show>>|Whether to show the autocompletion window.
|<<completion-height,height>>|The height of the completion, in px or as percentage of the window.
|<<completion-history-length,history-length>>|How many commands to save in the history.
|<<completion-cmd-history-max-items,cmd-history-max-items>>|How many commands to save in the command history.
|<<completion-web-history-max-items,web-history-max-items>>|How many URLs to show in the web history.
|<<completion-quick-complete,quick-complete>>|Whether to move on to the next part when there's only one possible completion left.
|<<completion-shrink,shrink>>|Whether to shrink the completion to be smaller than the configured size if there are no scrollbars.
|==============
@ -616,6 +618,12 @@ Valid values:
Default: +pass:[path]+
[[completion-timestamp-format]]
=== timestamp-format
How to format timestamps (e.g. for history)
Default: +pass:[%Y-%m-%d]+
[[completion-show]]
=== show
Whether to show the autocompletion window.
@ -633,14 +641,22 @@ The height of the completion, in px or as percentage of the window.
Default: +pass:[50%]+
[[completion-history-length]]
=== history-length
How many commands to save in the history.
[[completion-cmd-history-max-items]]
=== cmd-history-max-items
How many commands to save in the command history.
0: no history / -1: unlimited
Default: +pass:[100]+
[[completion-web-history-max-items]]
=== web-history-max-items
How many URLs to show in the web history.
0: no history / -1: unlimited
Default: +pass:[1000]+
[[completion-quick-complete]]
=== quick-complete
Whether to move on to the next part when there's only one possible completion left.

View File

@ -38,6 +38,7 @@ from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
import qutebrowser
import qutebrowser.resources # pylint: disable=unused-import
from qutebrowser.completion.models import instances as completionmodels
from qutebrowser.commands import cmdutils, runners
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import quickmarks, cookies, cache, adblock, history
@ -165,6 +166,7 @@ class Application(QApplication):
def _init_modules(self):
"""Initialize all 'modules' which need to be initialized."""
# pylint: disable=too-many-statements
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(self)
objreg.register('save-manager', save_manager)
@ -207,6 +209,8 @@ class Application(QApplication):
log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(self)
objreg.register('cache', diskcache)
log.init.debug("Initializing completions...")
completionmodels.init()
if not session_manager.exists(self._args.session):
log.init.debug("Initializing main window...")
window = mainwindow.MainWindow()

View File

@ -280,7 +280,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window',
completion=[usertypes.Completion.quickmark_by_url])
completion=[usertypes.Completion.url])
def openurl(self, url=None, bg=False, tab=False, window=False,
count: {'special': 'count'}=None):
"""Open a URL in the current/[count]th tab.

View File

@ -20,8 +20,9 @@
"""Simple history which gets written to disk."""
import time
import collections
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtWebKit import QWebHistoryInterface
from qutebrowser.utils import utils, objreg, standarddir
@ -35,19 +36,21 @@ class HistoryEntry:
Attributes:
atime: The time the page was accessed.
url: The URL which was accessed as string
url: The URL which was accessed as QUrl.
url_string: The URL which was accessed as string.
"""
def __init__(self, atime, url):
self.atime = atime
self.url = url
self.atime = float(atime)
self.url = QUrl(url)
self.url_string = url
def __repr__(self):
return utils.get_repr(self, constructor=True, atime=self.atime,
url=self.url)
url=self.url.toDisplayString())
def __str__(self):
return '{} {}'.format(int(self.atime), self.url)
return '{} {}'.format(int(self.atime), self.url_string)
@classmethod
def from_str(cls, s):
@ -61,41 +64,56 @@ class WebHistory(QWebHistoryInterface):
Attributes:
_lineparser: The AppendLineParser used to save the history.
_old_urls: A set of URLs read from the on-disk history.
_history_dict: An OrderedDict of URLs read from the on-disk history.
_new_history: A list of HistoryEntry items of the current session.
_saved_count: How many HistoryEntries have been written to disk.
_old_hit: How many times an URL was found in _old_urls.
_old_miss: How many times an URL was not found in _old_urls.
Signals:
item_about_to_be_added: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
item_added: Emitted after a new HistoryEntry is added.
arg: The new HistoryEntry.
"""
changed = pyqtSignal()
item_about_to_be_added = pyqtSignal(HistoryEntry)
item_added = pyqtSignal(HistoryEntry)
def __init__(self, parent=None):
super().__init__(parent)
self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self)
self._old_urls = set()
self._history_dict = collections.OrderedDict()
with self._lineparser.open():
for line in self._lineparser:
data = line.strip().split(maxsplit=1)
data = line.rstrip().split(maxsplit=1)
if not data:
# empty line
continue
_time, url = data
self._old_urls.add(url)
atime, url = data
# This de-duplicates history entries; only the latest
# entry for each URL is kept. If you want to keep
# information about previous hits change the items in
# old_urls to be lists or change HistoryEntry to have a
# list of atimes.
self._history_dict[url] = HistoryEntry(atime, url)
self._history_dict.move_to_end(url)
self._new_history = []
self._saved_count = 0
self._old_hit = 0
self._old_miss = 0
objreg.get('save-manager').add_saveable(
'history', self.save, self.changed)
'history', self.save, self.item_added)
def __repr__(self):
return utils.get_repr(self, new_length=len(self._new_history))
return utils.get_repr(self, length=len(self))
def __getitem__(self, key):
return self._new_history[key]
def __iter__(self):
return iter(self._history_dict.values())
def __len__(self):
return len(self._history_dict)
def get_recent(self):
"""Get the most recent history entries."""
old = self._lineparser.get_recent()
@ -116,8 +134,11 @@ class WebHistory(QWebHistoryInterface):
"""
if not config.get('general', 'private-browsing'):
entry = HistoryEntry(time.time(), url_string)
self.item_about_to_be_added.emit(entry)
self._new_history.append(entry)
self.changed.emit()
self._history_dict[url_string] = entry
self._history_dict.move_to_end(url_string)
self.item_added.emit(entry)
def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history.
@ -128,12 +149,7 @@ class WebHistory(QWebHistoryInterface):
Return:
True if the url is in the history, False otherwise.
"""
if url_string in self._old_urls:
self._old_hit += 1
return True
else:
self._old_miss += 1
return url_string in (entry.url for entry in self._new_history)
return url_string in self._history_dict
def init():

View File

@ -21,12 +21,10 @@
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config, configdata
from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.completion.models import completion as models
from qutebrowser.completion.models.sortfilter import (
CompletionFilterModel as CFM)
from qutebrowser.completion.models import instances
class Completer(QObject):
@ -34,7 +32,6 @@ class Completer(QObject):
"""Completer which manages completions in a CompletionView.
Attributes:
models: dict of available completion models.
_cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
@ -53,15 +50,6 @@ class Completer(QObject):
self._cmd.textEdited.connect(self.on_text_edited)
self._ignore_change = False
self._empty_item_idx = None
self._models = {
usertypes.Completion.option: {},
usertypes.Completion.value: {},
}
self._init_static_completions()
self._init_setting_completions()
self.init_quickmark_completions()
self.init_session_completion()
self._timer = QTimer()
self._timer.setSingleShot(True)
self._timer.setInterval(0)
@ -79,52 +67,6 @@ class Completer(QObject):
window=self._win_id)
return completion.model()
def _init_static_completions(self):
"""Initialize the static completion models."""
self._models[usertypes.Completion.command] = CFM(
models.CommandCompletionModel(self), self)
self._models[usertypes.Completion.helptopic] = CFM(
models.HelpCompletionModel(self), self)
def _init_setting_completions(self):
"""Initialize setting completion models."""
self._models[usertypes.Completion.section] = CFM(
models.SettingSectionCompletionModel(self), self)
self._models[usertypes.Completion.option] = {}
self._models[usertypes.Completion.value] = {}
for sectname in configdata.DATA:
model = models.SettingOptionCompletionModel(sectname, self)
self._models[usertypes.Completion.option][sectname] = CFM(
model, self)
self._models[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname].keys():
model = models.SettingValueCompletionModel(sectname, opt, self)
self._models[usertypes.Completion.value][sectname][opt] = CFM(
model, self)
@pyqtSlot()
def init_quickmark_completions(self):
"""Initialize quickmark completion models."""
try:
self._models[usertypes.Completion.quickmark_by_url].deleteLater()
self._models[usertypes.Completion.quickmark_by_name].deleteLater()
except KeyError:
pass
self._models[usertypes.Completion.quickmark_by_url] = CFM(
models.QuickmarkCompletionModel('url', self), self)
self._models[usertypes.Completion.quickmark_by_name] = CFM(
models.QuickmarkCompletionModel('name', self), self)
@pyqtSlot()
def init_session_completion(self):
"""Initialize session completion model."""
try:
self._models[usertypes.Completion.sessions].deleteLater()
except KeyError:
pass
self._models[usertypes.Completion.sessions] = CFM(
models.SessionCompletionModel(self), self)
def _get_completion_model(self, completion, parts, cursor_part):
"""Get a completion model based on an enum member.
@ -138,17 +80,17 @@ class Completer(QObject):
"""
if completion == usertypes.Completion.option:
section = parts[cursor_part - 1]
model = self._models[completion].get(section)
model = instances.get(completion).get(section)
elif completion == usertypes.Completion.value:
section = parts[cursor_part - 2]
option = parts[cursor_part - 1]
try:
model = self._models[completion][section][option]
model = instances.get(completion)[section][option]
except KeyError:
# No completion model for this section/option.
model = None
else:
model = self._models.get(completion)
model = instances.get(completion)
return model
def _filter_cmdline_parts(self, parts, cursor_part):
@ -198,7 +140,7 @@ class Completer(QObject):
"{}".format(parts, cursor_part))
if cursor_part == 0:
# '|' or 'set|'
return self._models[usertypes.Completion.command]
return instances.get(usertypes.Completion.command)
# delegate completion to command
try:
completions = cmdutils.cmd_dict[parts[0]].completion

View File

@ -195,7 +195,8 @@ class CompletionView(QTreeView):
self.setModel(model)
if sel_model is not None:
sel_model.deleteLater()
self.expandAll()
for i in range(model.rowCount()):
self.expand(model.index(i, 0))
self._resize_columns()
model.rowsRemoved.connect(self.maybe_resize_completion)
model.rowsInserted.connect(self.maybe_resize_completion)

View File

@ -29,7 +29,8 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem
from qutebrowser.utils import usertypes, qtutils
Role = usertypes.enum('Role', ['sort'], start=Qt.UserRole, is_int=True)
Role = usertypes.enum('Role', ['sort', 'userdata'], start=Qt.UserRole,
is_int=True)
class BaseCompletionModel(QStandardItemModel):
@ -60,7 +61,8 @@ class BaseCompletionModel(QStandardItemModel):
self.appendRow(cat)
return cat
def new_item(self, cat, name, desc='', misc=None, sort=None):
def new_item(self, cat, name, desc='', misc=None, sort=None,
userdata=None):
"""Add a new item to a category.
Args:
@ -69,10 +71,14 @@ class BaseCompletionModel(QStandardItemModel):
desc: The description of the item.
misc: Misc text to display.
sort: Data for the sort role (int).
userdata: User data to be added for the first column.
Return:
A (nameitem, descitem, miscitem) tuple.
"""
assert not isinstance(name, int)
assert not isinstance(desc, int)
assert not isinstance(misc, int)
nameitem = QStandardItem(name)
descitem = QStandardItem(desc)
if misc is None:
@ -85,6 +91,8 @@ class BaseCompletionModel(QStandardItemModel):
cat.setChild(idx, 2, miscitem)
if sort is not None:
nameitem.setData(sort, Role.sort)
if userdata is not None:
nameitem.setData(userdata, Role.userdata)
return nameitem, descitem, miscitem
def flags(self, index):

View File

@ -17,13 +17,12 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for different usages."""
"""CompletionModels for the config."""
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.config import config, configdata
from qutebrowser.utils import log, qtutils, objreg
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
@ -148,104 +147,3 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
if not ok:
raise ValueError("Setting data failed! (section: {}, option: {}, "
"value: {})".format(section, option, value))
class CommandCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all commands and descriptions."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((obj.name, obj.desc))
for name, cmd in config.section('aliases').items():
cmdlist.append((name, "Alias for '{}'".format(cmd)))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
class HelpCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with help topics."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
self._init_settings()
def _init_commands(self):
"""Fill completion with :command entries."""
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((':' + obj.name, obj.desc))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
def _init_settings(self):
"""Fill completion with section->option entries."""
cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items():
for optname in sectdata.keys():
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
self.new_item(cat, name, desc)
class QuickmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all quickmarks."""
# pylint: disable=abstract-method
def __init__(self, match_field='url', parent=None):
super().__init__(parent)
cat = self.new_category("Quickmarks")
quickmarks = objreg.get('quickmark-manager').marks.items()
if match_field == 'url':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_url, qm_name)
elif match_field == 'name':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_name, qm_url)
else:
raise ValueError("Invalid value '{}' for match_field!".format(
match_field))
class SessionCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with session names."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Sessions")
for name in objreg.get('session-manager').list_sessions():
self.new_item(cat, name)

View File

@ -0,0 +1,155 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Global instances of the completion models.
Module attributes:
_instances: An dict of available completions.
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
initialize completions.
"""
import functools
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
from qutebrowser.completion.models.sortfilter import (
CompletionFilterModel as CFM)
from qutebrowser.utils import objreg, usertypes, log, debug
from qutebrowser.config import configdata
_instances = {}
def _init_command_completion():
"""Initialize the command completion model."""
log.completion.debug("Initializing command completion.")
_instances[usertypes.Completion.command] = CFM(
miscmodels.CommandCompletionModel())
def _init_helptopic_completion():
"""Initialize the helptopic completion model."""
log.completion.debug("Initializing helptopic completion.")
_instances[usertypes.Completion.helptopic] = CFM(
miscmodels.HelpCompletionModel())
def _init_url_completion():
"""Initialize the URL completion model."""
log.completion.debug("Initializing URL completion.")
with debug.log_time(log.completion, 'URL completion init'):
_instances[usertypes.Completion.url] = CFM(
urlmodel.UrlCompletionModel(), dumb_sort=Qt.DescendingOrder)
def _init_setting_completions():
"""Initialize setting completion models."""
log.completion.debug("Initializing setting completion.")
_instances[usertypes.Completion.section] = CFM(
configmodel.SettingSectionCompletionModel())
_instances[usertypes.Completion.option] = {}
_instances[usertypes.Completion.value] = {}
for sectname in configdata.DATA:
model = configmodel.SettingOptionCompletionModel(sectname)
_instances[usertypes.Completion.option][sectname] = CFM(model)
_instances[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname].keys():
model = configmodel.SettingValueCompletionModel(sectname, opt)
_instances[usertypes.Completion.value][sectname][opt] = CFM(model)
@pyqtSlot()
def init_quickmark_completions():
"""Initialize quickmark completion models."""
log.completion.debug("Initializing quickmark completion.")
try:
_instances[usertypes.Completion.quickmark_by_url].deleteLater()
_instances[usertypes.Completion.quickmark_by_name].deleteLater()
except KeyError:
pass
_instances[usertypes.Completion.quickmark_by_url] = CFM(
miscmodels.QuickmarkCompletionModel('url'))
_instances[usertypes.Completion.quickmark_by_name] = CFM(
miscmodels.QuickmarkCompletionModel('name'))
@pyqtSlot()
def init_session_completion():
"""Initialize session completion model."""
log.completion.debug("Initializing session completion.")
try:
_instances[usertypes.Completion.sessions].deleteLater()
except KeyError:
pass
_instances[usertypes.Completion.sessions] = CFM(
miscmodels.SessionCompletionModel())
INITIALIZERS = {
usertypes.Completion.command: _init_command_completion,
usertypes.Completion.helptopic: _init_helptopic_completion,
usertypes.Completion.url: _init_url_completion,
usertypes.Completion.section: _init_setting_completions,
usertypes.Completion.option: _init_setting_completions,
usertypes.Completion.value: _init_setting_completions,
usertypes.Completion.quickmark_by_url: init_quickmark_completions,
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
usertypes.Completion.sessions: init_session_completion,
}
def get(completion):
"""Get a certain completion. Initializes the completion if needed."""
try:
return _instances[completion]
except KeyError:
if completion in INITIALIZERS:
INITIALIZERS[completion]()
return _instances[completion]
else:
raise
def update(completions):
"""Update an already existing completion.
Args:
completions: An iterable of usertypes.Completions.
"""
did_run = []
for completion in completions:
if completion in _instances:
func = INITIALIZERS[completion]
if func not in did_run:
func()
did_run.append(func)
def init():
"""Initialize completions. Note this only connects signals."""
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.changed.connect(
functools.partial(update, [usertypes.Completion.quickmark_by_url,
usertypes.Completion.quickmark_by_name]))
session_manager = objreg.get('session-manager')
session_manager.update_completion.connect(
functools.partial(update, [usertypes.Completion.sessions]))

View File

@ -0,0 +1,124 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Misc. CompletionModels."""
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
class CommandCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all commands and descriptions."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((obj.name, obj.desc))
for name, cmd in config.section('aliases').items():
cmdlist.append((name, "Alias for '{}'".format(cmd)))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
class HelpCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with help topics."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
self._init_settings()
def _init_commands(self):
"""Fill completion with :command entries."""
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((':' + obj.name, obj.desc))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
def _init_settings(self):
"""Fill completion with section->option entries."""
cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items():
for optname in sectdata.keys():
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
self.new_item(cat, name, desc)
class QuickmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all quickmarks."""
# pylint: disable=abstract-method
def __init__(self, match_field='url', parent=None):
super().__init__(parent)
cat = self.new_category("Quickmarks")
quickmarks = objreg.get('quickmark-manager').marks.items()
if match_field == 'url':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_url, qm_name)
elif match_field == 'name':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_name, qm_url)
else:
raise ValueError("Invalid value '{}' for match_field!".format(
match_field))
class SessionCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with session names."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Sessions")
for name in objreg.get('session-manager').list_sessions():
self.new_item(cat, name)

View File

@ -25,7 +25,7 @@ Contains:
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex, Qt
from qutebrowser.utils import log, qtutils
from qutebrowser.utils import log, qtutils, debug
from qutebrowser.completion.models import base as completion
@ -65,14 +65,15 @@ class CompletionFilterModel(QSortFilterProxyModel):
Args:
val: The value to set.
"""
self.pattern = val
self.invalidateFilter()
sortcol = 0
try:
self.srcmodel.sort(sortcol)
except NotImplementedError:
self.sort(sortcol)
self.invalidate()
with debug.log_time(log.completion, 'Setting filter pattern'):
self.pattern = val
self.invalidateFilter()
sortcol = 0
try:
self.srcmodel.sort(sortcol)
except NotImplementedError:
self.sort(sortcol)
self.invalidate()
def count(self):
"""Get the count of non-toplevel items currently visible.
@ -132,11 +133,15 @@ class CompletionFilterModel(QSortFilterProxyModel):
if parent == QModelIndex():
return True
idx = self.srcmodel.index(row, 0, parent)
qtutils.ensure_valid(idx)
if not idx.isValid():
# No entries in parent model
return False
data = self.srcmodel.data(idx)
# TODO more sophisticated filtering
if not self.pattern:
return True
if not data:
return False
return self.pattern.casefold() in data.casefold()
def intelligentLessThan(self, lindex, rindex):

View File

@ -0,0 +1,95 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for URLs."""
import datetime
from PyQt5.QtCore import pyqtSlot
from qutebrowser.utils import objreg, utils
from qutebrowser.completion.models import base
from qutebrowser.config import config
class UrlCompletionModel(base.BaseCompletionModel):
"""A model which combines quickmarks and web history URLs.
Used for the `open` command."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._quickmark_cat = self.new_category("Quickmarks")
self._history_cat = self.new_category("History")
quickmarks = objreg.get('quickmark-manager').marks.items()
self._history = objreg.get('web-history')
for qm_name, qm_url in quickmarks:
self.new_item(self._quickmark_cat, qm_url, qm_name)
max_history = config.get('completion', 'web-history-max-items')
history = utils.newest_slice(self._history, max_history)
for entry in history:
self._add_history_entry(entry)
self._history.item_about_to_be_added.connect(
self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps)
def _fmt_atime(self, atime):
"""Format an atime to a human-readable string."""
fmt = config.get('completion', 'timestamp-format')
if fmt is None:
return ''
return datetime.datetime.fromtimestamp(atime).strftime(fmt)
def _add_history_entry(self, entry):
"""Add a new history entry to the completion."""
self.new_item(self._history_cat, entry.url.toDisplayString(), "",
self._fmt_atime(entry.atime), sort=int(entry.atime),
userdata=entry.url)
@config.change_filter('completion', 'timestamp-format')
def reformat_timestamps(self):
"""Reformat the timestamps if the config option was changed."""
for i in range(self._history_cat.rowCount()):
name_item = self._history_cat.child(i, 0)
atime_item = self._history_cat.child(i, 2)
atime = name_item.data(base.Role.sort)
atime_item.setText(self._fmt_atime(atime))
@pyqtSlot(object)
def on_history_item_added(self, entry):
"""Slot called when a new history item was added."""
for i in range(self._history_cat.rowCount()):
name_item = self._history_cat.child(i, 0)
atime_item = self._history_cat.child(i, 2)
url = name_item.data(base.Role.userdata)
if url == entry.url:
atime_item.setText(self._fmt_atime(entry.atime))
name_item.setData(int(entry.atime), base.Role.sort)
break
else:
self._add_history_entry(entry)

View File

@ -186,7 +186,7 @@ def _init_misc():
from qutebrowser.misc import lineparser
command_history = lineparser.LimitLineParser(
standarddir.data(), 'cmd-history',
limit=('completion', 'history-length'),
limit=('completion', 'cmd-history-max-items'),
parent=objreg.get('config'))
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
@ -259,6 +259,7 @@ class ConfigManager(QObject):
('colors', 'tab.indicator.error'): 'tabs.indicator.error',
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
('tabs', 'auto-hide'): 'hide-auto',
('completion', 'history-length'): 'cmd-history-max-items',
}
DELETED_OPTIONS = [
('colors', 'tab.seperator'),

View File

@ -309,6 +309,10 @@ DATA = collections.OrderedDict([
SettingValue(typ.DownloadPathSuggestion(), 'path'),
"What to display in the download filename input."),
('timestamp-format',
SettingValue(typ.String(none_ok=True), '%Y-%m-%d'),
"How to format timestamps (e.g. for history)"),
('show',
SettingValue(typ.Bool(), 'true'),
"Whether to show the autocompletion window."),
@ -318,9 +322,14 @@ DATA = collections.OrderedDict([
"The height of the completion, in px or as percentage of the "
"window."),
('history-length',
('cmd-history-max-items',
SettingValue(typ.Int(minval=-1), '100'),
"How many commands to save in the history.\n\n"
"How many commands to save in the command history.\n\n"
"0: no history / -1: unlimited"),
('web-history-max-items',
SettingValue(typ.Int(minval=-1), '1000'),
"How many URLs to show in the web history.\n\n"
"0: no history / -1: unlimited"),
('quick-complete',

View File

@ -191,7 +191,6 @@ class MainWindow(QWidget):
completion_obj = self._get_object('completion')
tabs = self._get_object('tabbed-browser')
cmd = self._get_object('status-command')
completer = self._get_object('completer')
search_runner = self._get_object('search-runner')
message_bridge = self._get_object('message-bridge')
mode_manager = self._get_object('mode-manager')
@ -258,15 +257,6 @@ class MainWindow(QWidget):
completion_obj.on_clear_completion_selection)
cmd.hide_completion.connect(completion_obj.hide)
# quickmark completion
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.changed.connect(completer.init_quickmark_completions)
# sessions completion
session_manager = objreg.get('session-manager')
session_manager.update_completion.connect(
completer.init_session_completion)
@pyqtSlot()
def resize_completion(self):
"""Adjust completion according to config."""

View File

@ -19,7 +19,10 @@
"""Tests for qutebrowser.utils.debug."""
import re
import time
import unittest
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QStyle, QFrame
@ -148,5 +151,21 @@ class TestDebug(unittest.TestCase):
r"fake('foo\nbar')")
class TestLogTime(unittest.TestCase):
"""Test log_time."""
def test_log_time(self):
logger = logging.getLogger('qt-tests')
with self.assertLogs(logger, logging.DEBUG) as logged:
with debug.log_time(logger, action='foobar'):
time.sleep(0.1)
self.assertEqual(len(logged.records), 1)
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
match = pattern.match(logged.records[0].msg)
self.assertTrue(match)
duration = float(match.group(1))
self.assertAlmostEqual(duration, 0.1, delta=0.01)
if __name__ == '__main__':
unittest.main()

View File

@ -404,5 +404,57 @@ class ForceEncodingTests(unittest.TestCase):
self.assertEqual(utils.force_encoding(text, 'ascii'), 'hell? w?rld')
class NewestSliceTests(unittest.TestCase):
"""Test newest_slice."""
def test_count_minus_two(self):
"""Test with a count of -2."""
with self.assertRaises(ValueError):
utils.newest_slice([], -2)
def test_count_minus_one(self):
"""Test with a count of -1 (all elements)."""
items = range(20)
sliced = utils.newest_slice(items, -1)
self.assertEqual(list(sliced), list(items))
def test_count_zero(self):
"""Test with a count of 0 (no elements)."""
items = range(20)
sliced = utils.newest_slice(items, 0)
self.assertEqual(list(sliced), [])
def test_count_much_smaller(self):
"""Test with a count which is much smaller than the iterable."""
items = range(20)
sliced = utils.newest_slice(items, 5)
self.assertEqual(list(sliced), [15, 16, 17, 18, 19])
def test_count_smaller(self):
"""Test with a count which is exactly one smaller."""
items = range(5)
sliced = utils.newest_slice(items, 4)
self.assertEqual(list(sliced), [1, 2, 3, 4])
def test_count_equal(self):
"""Test with a count which is just as large as the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 5)
self.assertEqual(list(sliced), list(items))
def test_count_bigger(self):
"""Test with a count which is one bigger than the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 6)
self.assertEqual(list(sliced), list(items))
def test_count_much_bigger(self):
"""Test with a count which is much bigger than the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 50)
self.assertEqual(list(sliced), list(items))
if __name__ == '__main__':
unittest.main()

View File

@ -23,6 +23,8 @@ import re
import sys
import inspect
import functools
import datetime
import contextlib
from PyQt5.QtCore import QEvent, QMetaMethod
@ -248,3 +250,12 @@ def format_call(func, args=None, kwargs=None, full=True):
else:
name = func.__name__
return '{}({})'.format(name, _format_args(args, kwargs))
@contextlib.contextmanager
def log_time(logger, action='operation'):
started = datetime.datetime.now()
yield
finished = datetime.datetime.now()
delta = (finished - started).total_seconds()
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))

View File

@ -237,7 +237,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
# Available command completions
Completion = enum('Completion', ['command', 'section', 'option', 'value',
'helptopic', 'quickmark_by_url',
'quickmark_by_name', 'sessions'])
'quickmark_by_name', 'url', 'sessions'])
class Question(QObject):

View File

@ -27,6 +27,7 @@ import os.path
import collections
import functools
import contextlib
import itertools
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QKeySequence, QColor
@ -552,3 +553,22 @@ def force_encoding(text, encoding):
This replaces all chars not encodable with question marks.
"""
return text.encode(encoding, errors='replace').decode(encoding)
def newest_slice(iterable, count):
"""Get an iterable for the n newest items of the given iterable.
Args:
count: How many elements to get.
0: get no items:
n: get the n newest items
-1: get all items
"""
if count < -1:
raise ValueError("count can't be smaller than -1!")
elif count == 0:
return []
elif count == -1 or len(iterable) < count:
return iterable
else:
return itertools.islice(iterable, len(iterable) - count, len(iterable))