diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 594764c37..3359d07dc 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -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() diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index bc856d1a6..6b1309378 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -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 = '{}' - 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( '{}'.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) diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 626085d30..f9f6dc492 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -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}>'), - ('', "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', '', [(1, 3)]), + ('', "bc", [(0, 3)]), # https://github.com/qutebrowser/qutebrowser/issues/4199 - ('foo', "'foo'", "'{foo}'"), - ('x', "'x'", "'{x}'"), - ('lt', "' - ).replace( - '}', '' - ) - 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 + ])