mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-12-13 09:50:05 +01:00
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:
10
docs/utils.md
Normal file
10
docs/utils.md
Normal 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/).
|
||||
32
examples/code_highlight.py
Normal file
32
examples/code_highlight.py
Normal 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_()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
93
src/superqt/utils/_code_syntax_highlight.py
Normal file
93
src/superqt/utils/_code_syntax_highlight.py
Normal 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
|
||||
19
tests/test_code_highlight.py
Normal file
19
tests/test_code_highlight.py
Normal 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")
|
||||
Reference in New Issue
Block a user