mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-12-16 03:00: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:
|
packages = find:
|
||||||
install_requires =
|
install_requires =
|
||||||
packaging
|
packaging
|
||||||
|
pygments>=2.4.0
|
||||||
qtpy>=1.1.0
|
qtpy>=1.1.0
|
||||||
typing-extensions>=3.10.0.0
|
typing-extensions>=3.10.0.0
|
||||||
python_requires = >=3.7
|
python_requires = >=3.7
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
|
"CodeSyntaxHighlight",
|
||||||
"create_worker",
|
"create_worker",
|
||||||
"ensure_main_thread",
|
"ensure_main_thread",
|
||||||
"ensure_object_thread",
|
"ensure_object_thread",
|
||||||
@@ -15,7 +16,7 @@ __all__ = (
|
|||||||
"WorkerBase",
|
"WorkerBase",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ._code_syntax_highlight import CodeSyntaxHighlight
|
||||||
from ._ensure_thread import ensure_main_thread, ensure_object_thread
|
from ._ensure_thread import ensure_main_thread, ensure_object_thread
|
||||||
from ._message_handler import QMessageHandler
|
from ._message_handler import QMessageHandler
|
||||||
from ._misc import signals_blocked
|
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