Use QSyntaxHighlighter for completion.
This is a more "Qt" way of highlighting syntax, and works around the problems of #4199 without resorting to complicated html escaping. The tests are more straightforward with less mocking, but do involve testing a private class.
This commit is contained in:
parent
2eacf4bd94
commit
0fed563a02
@ -59,7 +59,6 @@ except ImportError:
|
||||
|
||||
import qutebrowser
|
||||
import qutebrowser.resources
|
||||
from qutebrowser.completion import completiondelegate
|
||||
from qutebrowser.completion.models import miscmodels
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import config, websettings, configfiles, configinit
|
||||
@ -452,9 +451,6 @@ def _init_modules(args, crash_handler):
|
||||
pre_text='Error initializing SQL')
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
log.init.debug("Initializing completion...")
|
||||
completiondelegate.init()
|
||||
|
||||
log.init.debug("Initializing command history...")
|
||||
cmdhistory.init()
|
||||
|
||||
|
@ -28,13 +28,27 @@ import html
|
||||
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
||||
from PyQt5.QtCore import QRectF, QSize, Qt
|
||||
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
||||
QAbstractTextDocumentLayout)
|
||||
QAbstractTextDocumentLayout, QSyntaxHighlighter,
|
||||
QTextCharFormat)
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import qtutils, jinja
|
||||
from qutebrowser.utils import qtutils
|
||||
|
||||
|
||||
_cached_stylesheet = None
|
||||
class _Highlighter(QSyntaxHighlighter):
|
||||
|
||||
def __init__(self, doc, pattern, color):
|
||||
super().__init__(doc)
|
||||
self._format = QTextCharFormat()
|
||||
self._format.setForeground(color)
|
||||
self._pattern = pattern
|
||||
|
||||
def highlightBlock(self, text):
|
||||
"""Override highlightBlock for custom highlighting."""
|
||||
for match in re.finditer(self._pattern, text):
|
||||
start, end = match.span()
|
||||
length = end - start
|
||||
self.setFormat(start, length, self._format)
|
||||
|
||||
|
||||
class CompletionItemDelegate(QStyledItemDelegate):
|
||||
@ -194,27 +208,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
self._doc.setDefaultTextOption(text_option)
|
||||
self._doc.setDocumentMargin(2)
|
||||
|
||||
assert _cached_stylesheet is not None
|
||||
self._doc.setDefaultStyleSheet(_cached_stylesheet)
|
||||
|
||||
if index.parent().isValid():
|
||||
view = self.parent()
|
||||
pattern = view.pattern
|
||||
columns_to_filter = index.model().columns_to_filter(index)
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
if index.column() in columns_to_filter and pattern:
|
||||
pat = '({})'.format(re.escape(pattern).replace(r'\ ', r'|'))
|
||||
parts = re.split(pat, self._opt.text, flags=re.IGNORECASE)
|
||||
fmt = '<span class="highlight">{}</span>'
|
||||
escape = lambda s: html.escape(s, quote=False)
|
||||
highlight = lambda s: fmt.format(escape(s))
|
||||
# matches are at every odd index
|
||||
text = ''.join([
|
||||
highlight(s) if i % 2 == 1 else escape(s)
|
||||
for i, s in enumerate(parts)
|
||||
])
|
||||
self._doc.setHtml(text)
|
||||
else:
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
pat = re.escape(pattern).replace(r'\ ', r'|')
|
||||
_Highlighter(self._doc, pat,
|
||||
config.val.colors.completion.match.fg)
|
||||
else:
|
||||
self._doc.setHtml(
|
||||
'<span style="font: {};">{}</span>'.format(
|
||||
@ -289,24 +291,3 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
self._draw_focus_rect()
|
||||
|
||||
self._painter.restore()
|
||||
|
||||
|
||||
@config.change_filter('colors.completion.match.fg', function=True)
|
||||
def _update_stylesheet():
|
||||
"""Update the cached stylesheet."""
|
||||
stylesheet = """
|
||||
.highlight {
|
||||
color: {{ conf.colors.completion.match.fg }};
|
||||
}
|
||||
"""
|
||||
with jinja.environment.no_autoescape():
|
||||
template = jinja.environment.from_string(stylesheet)
|
||||
|
||||
global _cached_stylesheet
|
||||
_cached_stylesheet = template.render(conf=config.val)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the cached stylesheet."""
|
||||
_update_stylesheet()
|
||||
config.instance.changed.connect(_update_stylesheet)
|
||||
|
@ -19,82 +19,33 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import QModelIndex, QObject
|
||||
from PyQt5.QtGui import QPainter
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QTextDocument
|
||||
|
||||
from qutebrowser.completion import completiondelegate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def painter():
|
||||
return mock.Mock(spec=QPainter)
|
||||
|
||||
|
||||
def _qt_mock(klass, mocker):
|
||||
m = mocker.patch.object(completiondelegate, klass, autospec=True)
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_style_option(mocker):
|
||||
return _qt_mock('QStyleOptionViewItem', mocker)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_text_document(mocker):
|
||||
return _qt_mock('QTextDocument', mocker)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def view():
|
||||
class FakeView(QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.pattern = None
|
||||
|
||||
return FakeView()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def delegate(mock_style_option, mock_text_document, config_stub, mocker, view):
|
||||
_qt_mock('QStyle', mocker)
|
||||
_qt_mock('QTextOption', mocker)
|
||||
_qt_mock('QAbstractTextDocumentLayout', mocker)
|
||||
completiondelegate._cached_stylesheet = ''
|
||||
delegate = completiondelegate.CompletionItemDelegate(parent=view)
|
||||
delegate.initStyleOption = mock.Mock()
|
||||
return delegate
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pat,txt_in,txt_out', [
|
||||
# { and } represent the open/close html tags for highlighting
|
||||
('foo', 'foo', '{foo}'),
|
||||
('foo', 'foobar', '{foo}bar'),
|
||||
('foo', 'barfoo', 'bar{foo}'),
|
||||
('foo', 'barfoobaz', 'bar{foo}baz'),
|
||||
('foo', 'barfoobazfoo', 'bar{foo}baz{foo}'),
|
||||
('foo', 'foofoo', '{foo}{foo}'),
|
||||
('a b', 'cadb', 'c{a}d{b}'),
|
||||
('foo', '<foo>', '<{foo}>'),
|
||||
('<a>', "<a>bc", '{<a>}bc'),
|
||||
@pytest.mark.parametrize('pat,txt,segments', [
|
||||
('foo', 'foo', [(0, 3)]),
|
||||
('foo', 'foobar', [(0, 3)]),
|
||||
('foo', 'barfoo', [(3, 3)]),
|
||||
('foo', 'barfoobaz', [(3, 3)]),
|
||||
('foo', 'barfoobazfoo', [(3, 3), (9, 3)]),
|
||||
('foo', 'foofoo', [(0, 3), (3, 3)]),
|
||||
('a|b', 'cadb', [(1, 1), (3, 1)]),
|
||||
('foo', '<foo>', [(1, 3)]),
|
||||
('<a>', "<a>bc", [(0, 3)]),
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/4199
|
||||
('foo', "'foo'", "'{foo}'"),
|
||||
('x', "'x'", "'{x}'"),
|
||||
('lt', "<lt", "<{lt}"),
|
||||
('foo', "'foo'", [(1, 3)]),
|
||||
('x', "'x'", [(1, 1)]),
|
||||
('lt', "<lt", [(1, 2)]),
|
||||
])
|
||||
def test_paint(delegate, painter, view, mock_style_option, mock_text_document,
|
||||
pat, txt_in, txt_out):
|
||||
view.pattern = pat
|
||||
mock_style_option().text = txt_in
|
||||
index = mock.Mock(spec=QModelIndex)
|
||||
index.column.return_value = 0
|
||||
index.model.return_value.columns_to_filter.return_value = [0]
|
||||
opt = mock_style_option()
|
||||
delegate.paint(painter, opt, index)
|
||||
expected = txt_out.replace(
|
||||
'{', '<span class="highlight">'
|
||||
).replace(
|
||||
'}', '</span>'
|
||||
)
|
||||
mock_text_document().setHtml.assert_called_once_with(expected)
|
||||
def test_highlight(pat, txt, segments):
|
||||
doc = QTextDocument(txt)
|
||||
highlighter = completiondelegate._Highlighter(doc, pat, Qt.red)
|
||||
highlighter.setFormat = mock.Mock()
|
||||
highlighter.highlightBlock(txt)
|
||||
highlighter.setFormat.assert_has_calls([
|
||||
mock.call(s[0], s[1], mock.ANY) for s in segments
|
||||
])
|
||||
|
Loading…
Reference in New Issue
Block a user