Add code syntax highlight utils (#88)

* add code syntax highlight code

* add example

* add documentation and fix example

* add tests

* add information about napari theme usage

* clean napari mention
This commit is contained in:
Grzegorz Bokota
2022-05-18 22:50:51 +02:00
committed by GitHub
parent c7f8780900
commit 0ae3350c57
6 changed files with 157 additions and 1 deletions

10
docs/utils.md Normal file
View File

@@ -0,0 +1,10 @@
# Utils
## Code highlighting
`superqt` provides a code highlighter subclass of `QSyntaxHighlighter`
that can be used to highlight code in a QTextEdit.
Code lexer and available styles are from [`pygments`](https://pygments.org/) python library
List of available languages are available [here](https://pygments.org/languages/).
List of available styles are available [here](https://pygments.org/styles/).

View File

@@ -0,0 +1,32 @@
from PyQt5.QtGui import QColor, QPalette
from qtpy.QtWidgets import QApplication, QTextEdit
from superqt.utils import CodeSyntaxHighlight
app = QApplication([])
text_area = QTextEdit()
highlight = CodeSyntaxHighlight(text_area.document(), "python", "monokai")
palette = text_area.palette()
palette.setColor(QPalette.Base, QColor(highlight.background_color))
text_area.setPalette(palette)
text_area.setText(
"""from argparse import ArgumentParser
def main():
parser = ArgumentParser()
parser.add_argument("name", help="Your name")
args = parser.parse_args()
print(f"Hello {args.name}")
if __name__ == "__main__":
main()
"""
)
text_area.show()
app.exec_()

View File

@@ -36,6 +36,7 @@ project_urls =
packages = find:
install_requires =
packaging
pygments>=2.4.0
qtpy>=1.1.0
typing-extensions>=3.10.0.0
python_requires = >=3.7

View File

@@ -1,4 +1,5 @@
__all__ = (
"CodeSyntaxHighlight",
"create_worker",
"ensure_main_thread",
"ensure_object_thread",
@@ -15,7 +16,7 @@ __all__ = (
"WorkerBase",
)
from ._code_syntax_highlight import CodeSyntaxHighlight
from ._ensure_thread import ensure_main_thread, ensure_object_thread
from ._message_handler import QMessageHandler
from ._misc import signals_blocked

View File

@@ -0,0 +1,93 @@
from itertools import takewhile
from pygments import highlight
from pygments.formatter import Formatter
from pygments.lexers import find_lexer_class, get_lexer_by_name
from pygments.util import ClassNotFound
from qtpy import QtGui
# inspired by https://github.com/Vector35/snippets/blob/master/QCodeEditor.py (MIT license) and
# https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
def get_text_char_format(style):
"""
Return a QTextCharFormat with the given attributes.
https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
"""
text_char_format = QtGui.QTextCharFormat()
text_char_format.setFontFamily("monospace")
if style.get("color"):
text_char_format.setForeground(QtGui.QColor(f"#{style['color']}"))
if style.get("bgcolor"):
text_char_format.setBackground(QtGui.QColor(style["bgcolor"]))
if style.get("bold"):
text_char_format.setFontWeight(QtGui.QFont.Bold)
if style.get("italic"):
text_char_format.setFontItalic(True)
if style.get("underline"):
text_char_format.setFontUnderline(True)
# TODO find if it is possible to support border style.
return text_char_format
class QFormatter(Formatter):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.data = []
self._style = {name: get_text_char_format(style) for name, style in self.style}
def format(self, tokensource, outfile):
"""
`outfile` is argument from parent class, but
in Qt we do not produce string output, but QTextCharFormat, so it needs to be
collected using `self.data`.
"""
self.data = []
for token, value in tokensource:
self.data.extend(
[
self._style[token],
]
* len(value)
)
class CodeSyntaxHighlight(QtGui.QSyntaxHighlighter):
def __init__(self, parent, lang, theme):
super().__init__(parent)
self.formatter = QFormatter(style=theme)
try:
self.lexer = get_lexer_by_name(lang)
except ClassNotFound:
self.lexer = find_lexer_class(lang)()
@property
def background_color(self):
return self.formatter.style.background_color
def highlightBlock(self, text):
cb = self.currentBlock()
p = cb.position()
text_ = self.document().toPlainText() + "\n"
highlight(text_, self.lexer, self.formatter)
enters = sum(1 for _ in takewhile(lambda x: x == "\n", text_))
# pygments lexer ignore leading empty lines, so we need to do correction
# here calculating the number of empty lines.
# dirty, dirty hack
# The core problem is that pygemnts by default use string streams,
# that will not handle QTextCharFormat, so wee need use `data` property to work around this.
for i in range(len(text)):
try:
self.setFormat(i, 1, self.formatter.data[p + i - enters])
except IndexError: # pragma: no cover
pass

View File

@@ -0,0 +1,19 @@
from qtpy.QtWidgets import QTextEdit
from superqt.utils import CodeSyntaxHighlight
def test_code_highlight(qtbot):
widget = QTextEdit()
qtbot.addWidget(widget)
code_highlight = CodeSyntaxHighlight(widget, "python", "default")
assert code_highlight.background_color == "#f8f8f8"
widget.setText("from argparse import ArgumentParser")
def test_code_highlight_by_name(qtbot):
widget = QTextEdit()
qtbot.addWidget(widget)
code_highlight = CodeSyntaxHighlight(widget, "Python Traceback", "monokai")
assert code_highlight.background_color == "#272822"
widget.setText("from argparse import ArgumentParser")