Merge remote-tracking branch 'origin/pr/4180' into completion

This commit is contained in:
Florian Bruhin 2020-07-29 13:02:01 +02:00
commit f86cd440de
10 changed files with 208 additions and 16 deletions

View File

@ -145,10 +145,13 @@ This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded host lis
[[back]]
=== back
Syntax: +:back [*--tab*] [*--bg*] [*--window*]+
Syntax: +:back [*--tab*] [*--bg*] [*--window*] ['index']+
Go back in the history of the current tab.
==== positional arguments
* +'index'+: Which page to go back to, count takes precedence.
==== optional arguments
* +*-t*+, +*--tab*+: Go back in a new tab.
* +*-b*+, +*--bg*+: Go back in a background tab.
@ -567,10 +570,13 @@ Follow the selected text.
[[forward]]
=== forward
Syntax: +:forward [*--tab*] [*--bg*] [*--window*]+
Syntax: +:forward [*--tab*] [*--bg*] [*--window*] ['index']+
Go forward in the history of the current tab.
==== positional arguments
* +'index'+: Which page to go forward to, count takes precedence.
==== optional arguments
* +*-t*+, +*--tab*+: Go forward in a new tab.
* +*-b*+, +*--bg*+: Go forward in a background tab.

View File

@ -689,6 +689,12 @@ class AbstractHistory:
def _go_to_item(self, item: typing.Any) -> None:
raise NotImplementedError
def back_items(self) -> typing.List[typing.Any]:
raise NotImplementedError
def forward_items(self) -> typing.List[typing.Any]:
raise NotImplementedError
class AbstractElements:

View File

@ -498,7 +498,7 @@ class CommandDispatcher:
self._tabbed_browser.close_tab(self._current_widget(),
add_undo=False)
def _back_forward(self, tab, bg, window, count, forward):
def _back_forward(self, tab, bg, window, count, forward, index=None):
"""Helper function for :back/:forward."""
history = self._current_widget().history
# Catch common cases before e.g. cloning tab
@ -512,6 +512,12 @@ class CommandDispatcher:
else:
widget = self._current_widget()
if count is None:
if index is None:
count = 1
else:
count = abs(history.current_idx() - index)
try:
if forward:
widget.history.forward(count)
@ -522,7 +528,9 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', value=cmdutils.Value.count)
def back(self, tab=False, bg=False, window=False, count=1):
@cmdutils.argument('index', completion=miscmodels.back)
def back(self, tab=False, bg=False, window=False,
count=None, index: int = None):
"""Go back in the history of the current tab.
Args:
@ -530,12 +538,15 @@ class CommandDispatcher:
bg: Go back in a background tab.
window: Go back in a new window.
count: How many pages to go back.
index: Which page to go back to, count takes precedence.
"""
self._back_forward(tab, bg, window, count, forward=False)
self._back_forward(tab, bg, window, count, forward=False, index=index)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', value=cmdutils.Value.count)
def forward(self, tab=False, bg=False, window=False, count=1):
@cmdutils.argument('index', completion=miscmodels.forward)
def forward(self, tab=False, bg=False, window=False,
count=None, index: int = None):
"""Go forward in the history of the current tab.
Args:
@ -543,8 +554,9 @@ class CommandDispatcher:
bg: Go forward in a background tab.
window: Go forward in a new window.
count: How many pages to go forward.
index: Which page to go forward to, count takes precedence.
"""
self._back_forward(tab, bg, window, count, forward=True)
self._back_forward(tab, bg, window, count, forward=True, index=index)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',

View File

@ -97,7 +97,14 @@ def _serialize_item(item, stream):
## static_cast<qint64>(entry->GetTimestamp().ToInternalValue());
# \x00\x00\x00\x00^\x97$\xe7
stream.writeInt64(int(time.time()))
if item.last_visited is None:
unix_msecs = 0
else:
unix_msecs = item.last_visited.toMSecsSinceEpoch()
# 11644516800000 is the number of milliseconds from
# 1601-01-01T00:00 (Windows NT Epoch) to 1970-01-01T00:00 (UNIX Epoch)
nt_usecs = (unix_msecs + 11644516800000) * 1000
stream.writeInt64(nt_usecs)
## entry->GetHttpStatusCode();
# \x00\x00\x00\xc8
stream.writeInt(200)

View File

@ -755,6 +755,12 @@ class WebEngineHistory(browsertab.AbstractHistory):
self._tab.before_load_started.emit(item.url())
self._history.goToItem(item)
def back_items(self):
return self._history.backItems(self._history.count())
def forward_items(self):
return self._history.forwardItems(self._history.count())
class WebEngineZoom(browsertab.AbstractZoom):

View File

@ -658,6 +658,12 @@ class WebKitHistory(browsertab.AbstractHistory):
self._tab.before_load_started.emit(item.url())
self._history.goToItem(item)
def back_items(self):
return self._history.backItems(self._history.count())
def forward_items(self):
return self._history.forwardItems(self._history.count())
class WebKitElements(browsertab.AbstractElements):

View File

@ -154,7 +154,7 @@ class CompletionModel(QAbstractItemModel):
def columnCount(self, parent=QModelIndex()):
"""Override QAbstractItemModel::columnCount."""
# pylint: disable=unused-argument
return 3
return len(self.column_widths)
def canFetchMore(self, parent):
"""Override to forward the call to the categories."""

View File

@ -19,6 +19,7 @@
"""Functions that return miscellaneous completion models."""
import datetime
import typing
from qutebrowser.config import config, configdata
@ -216,3 +217,59 @@ def inspector_position(*, info):
category = listcategory.ListCategory("Position (optional)", positions)
model.add_category(category)
return model
def _qdatetime_to_completion_format(qdate):
if not qdate.isValid():
ts = 0
else:
ts = qdate.toSecsSinceEpoch()
if ts < 0:
ts = 0
pydate = datetime.datetime.fromtimestamp(ts)
return pydate.strftime(config.val.completion.timestamp_format)
def _back_forward(info, go_forward):
tab = objreg.get('tab', scope='tab', window=info.win_id, tab='current')
history = tab.history
current_idx = history.current_idx()
model = completionmodel.CompletionModel(column_widths=(5, 36, 50, 9))
if go_forward:
start = current_idx + 1
items = history.forward_items()
else:
start = 0
items = history.back_items()
entries = [
(
str(idx),
entry.url().toDisplayString(),
entry.title(),
_qdatetime_to_completion_format(entry.lastVisited())
)
for idx, entry in enumerate(items, start)
]
if not go_forward:
# make sure the most recent is at the top for :back
entries = reversed(entries)
cat = listcategory.ListCategory("History", entries, sort=False)
model.add_category(cat)
return model
def forward(*, info):
"""A model to complete on history of the current tab.
Used for the :forward command.
"""
return _back_forward(info, go_forward=True)
def back(*, info):
"""A model to complete on history of the current tab.
Used for the :back command.
"""
return _back_forward(info, go_forward=False)

View File

@ -27,7 +27,7 @@ import typing
import glob
import shutil
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
from PyQt5.QtCore import Qt, QUrl, QObject, QPoint, QTimer, QDateTime
from PyQt5.QtWidgets import QApplication
import yaml
@ -121,7 +121,7 @@ class TabHistoryItem:
"""
def __init__(self, url, title, *, original_url=None, active=False,
user_data=None):
user_data=None, last_visited=None):
self.url = url
if original_url is None:
self.original_url = url
@ -130,11 +130,13 @@ class TabHistoryItem:
self.title = title
self.active = active
self.user_data = user_data
self.last_visited = last_visited
def __repr__(self):
return utils.get_repr(self, constructor=True, url=self.url,
original_url=self.original_url, title=self.title,
active=self.active, user_data=self.user_data)
active=self.active, user_data=self.user_data,
last_visited=self.last_visited)
class SessionManager(QObject):
@ -220,6 +222,8 @@ class SessionManager(QObject):
# QtWebEngine
user_data = None
data['last_visited'] = item.lastVisited().toString(Qt.ISODate)
if tab.history.current_idx() == idx:
pos = tab.scroller.pos_px()
data['zoom'] = tab.zoom.factor()
@ -429,9 +433,17 @@ class SessionManager(QObject):
histentry['original-url'].encode('ascii'))
else:
orig_url = url
if histentry.get("last_visited"):
last_visited = QDateTime.fromString(
histentry.get("last_visited"),
Qt.ISODate,
)
else:
last_visited = None
entry = TabHistoryItem(url=url, original_url=orig_url,
title=histentry['title'], active=active,
user_data=user_data)
user_data=user_data,
last_visited=last_visited)
entries.append(entry)
if active:
new_tab.title_changed.emit(histentry['title'])

View File

@ -22,10 +22,18 @@
import collections
import random
import string
import time
from datetime import datetime
from unittest import mock
import pytest
from PyQt5.QtCore import QUrl
from PyQt5.QtCore import QUrl, QDateTime
try:
from PyQt5.QtWebEngineWidgets import (
QWebEngineHistory, QWebEngineHistoryItem
)
except ImportError:
pass
from qutebrowser.misc import objects
from qutebrowser.completion import completer
@ -35,7 +43,7 @@ from qutebrowser.utils import usertypes
def _check_completions(model, expected):
"""Check that a model contains the expected items in any order.
"""Check that a model contains the expected items in order.
Args:
expected: A dict of form
@ -59,7 +67,6 @@ def _check_completions(model, expected):
actual[catname].append((name, desc, misc))
assert actual == expected
# sanity-check the column_widths
assert len(model.column_widths) == 3
assert sum(model.column_widths) == 100
@ -1179,3 +1186,76 @@ def test_url_completion_benchmark(benchmark, info,
model.set_pattern('ex 123')
benchmark(bench)
@pytest.fixture
def tab_with_history(fake_web_tab, tabbed_browser_stubs, info, monkeypatch):
"""Returns a fake tab with some fake history items."""
pytest.importorskip('PyQt5.QtWebEngineWidgets')
tab = fake_web_tab(QUrl('https://github.com'), 'GitHub', 0)
current_idx = 2
monkeypatch.setattr(
tab.history, 'current_idx',
lambda: current_idx,
)
history = []
now = time.time()
for url, title, ts in [
("http://example.com/index", "list of things", now),
("http://example.com/thing1", "thing1 detail", now+5),
("http://example.com/thing2", "thing2 detail", now+10),
("http://example.com/thing3", "thing3 detail", now+15),
("http://example.com/thing4", "thing4 detail", now+20),
]:
entry = mock.Mock(spec=QWebEngineHistoryItem)
entry.url.return_value = QUrl(url)
entry.title.return_value = title
entry.lastVisited.return_value = QDateTime.fromSecsSinceEpoch(ts)
history.append(entry)
tab.history._history = mock.Mock(spec=QWebEngineHistory)
tab.history._history.items.return_value = history
monkeypatch.setattr(
tab.history, 'back_items',
lambda *_args: (
entry for idx, entry in enumerate(tab.history._history.items())
if idx < current_idx
),
)
monkeypatch.setattr(
tab.history, 'forward_items',
lambda *_args: (
entry for idx, entry in enumerate(tab.history._history.items())
if idx > current_idx
),
)
tabbed_browser_stubs[0].widget.tabs = [tab]
tabbed_browser_stubs[0].widget.current_index = 0
return tab
def test_back_completion(tab_with_history, info):
"""Test back tab history completion."""
model = miscmodels.back(info=info)
model.set_pattern('')
_check_completions(model, {
"History": [
("1", "http://example.com/thing1", "thing1 detail"),
("0", "http://example.com/index", "list of things"),
],
})
def test_forward_completion(tab_with_history, info):
"""Test forward tab history completion."""
model = miscmodels.forward(info=info)
model.set_pattern('')
_check_completions(model, {
"History": [
("3", "http://example.com/thing3", "thing3 detail"),
("4", "http://example.com/thing4", "thing4 detail"),
],
})