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
|
||||||
import qutebrowser.resources
|
import qutebrowser.resources
|
||||||
from qutebrowser.completion import completiondelegate
|
|
||||||
from qutebrowser.completion.models import miscmodels
|
from qutebrowser.completion.models import miscmodels
|
||||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||||
from qutebrowser.config import config, websettings, configfiles, configinit
|
from qutebrowser.config import config, websettings, configfiles, configinit
|
||||||
@ -452,9 +451,6 @@ def _init_modules(args, crash_handler):
|
|||||||
pre_text='Error initializing SQL')
|
pre_text='Error initializing SQL')
|
||||||
sys.exit(usertypes.Exit.err_init)
|
sys.exit(usertypes.Exit.err_init)
|
||||||
|
|
||||||
log.init.debug("Initializing completion...")
|
|
||||||
completiondelegate.init()
|
|
||||||
|
|
||||||
log.init.debug("Initializing command history...")
|
log.init.debug("Initializing command history...")
|
||||||
cmdhistory.init()
|
cmdhistory.init()
|
||||||
|
|
||||||
|
@ -28,13 +28,27 @@ import html
|
|||||||
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
||||||
from PyQt5.QtCore import QRectF, QSize, Qt
|
from PyQt5.QtCore import QRectF, QSize, Qt
|
||||||
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
||||||
QAbstractTextDocumentLayout)
|
QAbstractTextDocumentLayout, QSyntaxHighlighter,
|
||||||
|
QTextCharFormat)
|
||||||
|
|
||||||
from qutebrowser.config import config
|
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):
|
class CompletionItemDelegate(QStyledItemDelegate):
|
||||||
@ -194,27 +208,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
|||||||
self._doc.setDefaultTextOption(text_option)
|
self._doc.setDefaultTextOption(text_option)
|
||||||
self._doc.setDocumentMargin(2)
|
self._doc.setDocumentMargin(2)
|
||||||
|
|
||||||
assert _cached_stylesheet is not None
|
|
||||||
self._doc.setDefaultStyleSheet(_cached_stylesheet)
|
|
||||||
|
|
||||||
if index.parent().isValid():
|
if index.parent().isValid():
|
||||||
view = self.parent()
|
view = self.parent()
|
||||||
pattern = view.pattern
|
pattern = view.pattern
|
||||||
columns_to_filter = index.model().columns_to_filter(index)
|
columns_to_filter = index.model().columns_to_filter(index)
|
||||||
|
self._doc.setPlainText(self._opt.text)
|
||||||
if index.column() in columns_to_filter and pattern:
|
if index.column() in columns_to_filter and pattern:
|
||||||
pat = '({})'.format(re.escape(pattern).replace(r'\ ', r'|'))
|
pat = re.escape(pattern).replace(r'\ ', r'|')
|
||||||
parts = re.split(pat, self._opt.text, flags=re.IGNORECASE)
|
_Highlighter(self._doc, pat,
|
||||||
fmt = '<span class="highlight">{}</span>'
|
config.val.colors.completion.match.fg)
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
self._doc.setHtml(
|
self._doc.setHtml(
|
||||||
'<span style="font: {};">{}</span>'.format(
|
'<span style="font: {};">{}</span>'.format(
|
||||||
@ -289,24 +291,3 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
|||||||
self._draw_focus_rect()
|
self._draw_focus_rect()
|
||||||
|
|
||||||
self._painter.restore()
|
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
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PyQt5.QtCore import QModelIndex, QObject
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QPainter
|
from PyQt5.QtGui import QTextDocument
|
||||||
|
|
||||||
from qutebrowser.completion import completiondelegate
|
from qutebrowser.completion import completiondelegate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.mark.parametrize('pat,txt,segments', [
|
||||||
def painter():
|
('foo', 'foo', [(0, 3)]),
|
||||||
return mock.Mock(spec=QPainter)
|
('foo', 'foobar', [(0, 3)]),
|
||||||
|
('foo', 'barfoo', [(3, 3)]),
|
||||||
|
('foo', 'barfoobaz', [(3, 3)]),
|
||||||
def _qt_mock(klass, mocker):
|
('foo', 'barfoobazfoo', [(3, 3), (9, 3)]),
|
||||||
m = mocker.patch.object(completiondelegate, klass, autospec=True)
|
('foo', 'foofoo', [(0, 3), (3, 3)]),
|
||||||
return m
|
('a|b', 'cadb', [(1, 1), (3, 1)]),
|
||||||
|
('foo', '<foo>', [(1, 3)]),
|
||||||
|
('<a>', "<a>bc", [(0, 3)]),
|
||||||
@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'),
|
|
||||||
|
|
||||||
# https://github.com/qutebrowser/qutebrowser/issues/4199
|
# https://github.com/qutebrowser/qutebrowser/issues/4199
|
||||||
('foo', "'foo'", "'{foo}'"),
|
('foo', "'foo'", [(1, 3)]),
|
||||||
('x', "'x'", "'{x}'"),
|
('x', "'x'", [(1, 1)]),
|
||||||
('lt', "<lt", "<{lt}"),
|
('lt', "<lt", [(1, 2)]),
|
||||||
])
|
])
|
||||||
def test_paint(delegate, painter, view, mock_style_option, mock_text_document,
|
def test_highlight(pat, txt, segments):
|
||||||
pat, txt_in, txt_out):
|
doc = QTextDocument(txt)
|
||||||
view.pattern = pat
|
highlighter = completiondelegate._Highlighter(doc, pat, Qt.red)
|
||||||
mock_style_option().text = txt_in
|
highlighter.setFormat = mock.Mock()
|
||||||
index = mock.Mock(spec=QModelIndex)
|
highlighter.highlightBlock(txt)
|
||||||
index.column.return_value = 0
|
highlighter.setFormat.assert_has_calls([
|
||||||
index.model.return_value.columns_to_filter.return_value = [0]
|
mock.call(s[0], s[1], mock.ANY) for s in segments
|
||||||
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)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user