1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
|
"""
This module contains the syntax highlighter API.
"""
import logging
import sys
import time
import weakref
from pygments.styles import get_style_by_name, get_all_styles
from pygments.token import Token, Punctuation
from pygments.util import ClassNotFound
from pyqode.core.api.mode import Mode
from pyqode.core.api.utils import drift_color
from pyqode.qt import QtGui, QtCore, QtWidgets
def _logger():
return logging.getLogger(__name__)
#: A sorted list of available pygments styles, for convenience
PYGMENTS_STYLES = sorted(set(list(get_all_styles()) + ['darcula', 'qt']))
#: The list of color schemes keys (and their associated pygments token)
COLOR_SCHEME_KEYS = {
# editor background
"background": None,
# highlight color (used for caret line)
"highlight": None,
# normal text
"normal": Token.Text,
# any keyword
"keyword": Token.Keyword,
# namespace keywords (from ... import ... as)
"namespace": Token.Keyword.Namespace,
# type keywords
"type": Token.Keyword.Type,
# reserved keyword
"keyword_reserved": Token.Keyword.Reserved,
# any builtin name
"builtin": Token.Name.Builtin,
# any definition (class or function)
"definition": Token.Name.Class,
# any comment
"comment": Token.Comment,
# any string
"string": Token.Literal.String,
# any docstring (python docstring, c++ doxygen comment,...)
"docstring": Token.Literal.String.Doc,
# any number
"number": Token.Number,
# any instance variable
"instance": Token.Name.Variable,
# whitespace color
"whitespace": Token.Text.Whitespace,
# any tag name (e.g. shinx doctags,...)
'tag': Token.Name.Tag,
# self paramter (or this in other languages)
'self': Token.Name.Builtin.Pseudo,
# python decorators
'decorator': Token.Name.Decorator,
# colors of punctuation characters
'punctuation': Punctuation,
# name or keyword constant
'constant': Token.Name.Constant,
# function definition
'function': Token.Name.Function,
# operator
'operator': Token.Operator,
# operator words (and, not)
'operator_word': Token.Operator.Word
}
class ColorScheme(object):
"""
Translates a pygments style into a dictionary of colors associated with a
style key.
See :attr:`pyqode.core.api.syntax_highligter.COLOR_SCHEM_KEYS` for the
available keys.
"""
@property
def name(self):
"""
Name of the color scheme, this is usually the name of the associated
pygments style.
"""
return self._name
@property
def background(self):
"""
Gets the background color.
:return:
"""
return self.formats['background'].background().color()
@property
def highlight(self):
"""
Gets the highlight color.
:return:
"""
return self.formats['highlight'].background().color()
def __init__(self, style):
"""
:param style: name of the pygments style to load
"""
self._name = style
self._brushes = {}
#: Dictionary of formats colors (keys are the same as for
#: :attr:`pyqode.core.api.COLOR_SCHEME_KEYS`
self.formats = {}
try:
style = get_style_by_name(style)
except ClassNotFound:
if style == 'darcula':
from pyqode.core.styles.darcula import DarculaStyle
style = DarculaStyle
else:
from pyqode.core.styles.qt import QtStyle
style = QtStyle
self._load_formats_from_style(style)
def _load_formats_from_style(self, style):
# background
self.formats['background'] = self._get_format_from_color(
style.background_color)
# highlight
self.formats['highlight'] = self._get_format_from_color(
style.highlight_color)
for key, token in COLOR_SCHEME_KEYS.items():
if token and key:
self.formats[key] = self._get_format_from_style(token, style)
def _get_format_from_color(self, color):
fmt = QtGui.QTextCharFormat()
fmt.setBackground(self._get_brush(color))
return fmt
def _get_format_from_style(self, token, style):
""" Returns a QTextCharFormat for token by reading a Pygments style.
"""
result = QtGui.QTextCharFormat()
items = list(style.style_for_token(token).items())
for key, value in items:
if value is None and key == 'color':
# make sure to use a default visible color for the foreground
# brush
value = drift_color(self.background, 1000).name()
if value:
if key == 'color':
result.setForeground(self._get_brush(value))
elif key == 'bgcolor':
result.setBackground(self._get_brush(value))
elif key == 'bold':
result.setFontWeight(QtGui.QFont.Bold)
elif key == 'italic':
result.setFontItalic(value)
elif key == 'underline':
result.setUnderlineStyle(
QtGui.QTextCharFormat.SingleUnderline)
elif key == 'sans':
result.setFontStyleHint(QtGui.QFont.SansSerif)
elif key == 'roman':
result.setFontStyleHint(QtGui.QFont.Times)
elif key == 'mono':
result.setFontStyleHint(QtGui.QFont.TypeWriter)
if token in [Token.Literal.String, Token.Literal.String.Doc,
Token.Comment]:
# mark strings, comments and docstrings regions for further queries
result.setObjectType(result.UserObject)
return result
def _get_brush(self, color):
""" Returns a brush for the color.
"""
result = self._brushes.get(color)
if result is None:
qcolor = self._get_color(color)
result = QtGui.QBrush(qcolor)
self._brushes[color] = result
return result
@staticmethod
def _get_color(color):
""" Returns a QColor built from a Pygments color string. """
color = str(color).replace("#", "")
qcolor = QtGui.QColor()
qcolor.setRgb(int(color[:2], base=16),
int(color[2:4], base=16),
int(color[4:6], base=16))
return qcolor
class SyntaxHighlighter(QtGui.QSyntaxHighlighter, Mode):
"""
Abstract base class for syntax highlighter modes.
It fills up the document with our custom block data (fold levels,
triggers,...).
It **does not do any syntax highlighting**, that task is left to
sublasses such as :class:`pyqode.core.modes.PygmentsSyntaxHighlighter`.
Subclasses **must** override the
:meth:`pyqode.core.api.SyntaxHighlighter.highlight_block` method to
apply custom highlighting.
.. note:: Since version 2.1 and for performance reasons, we store all
our data in the block user state as a bit-mask. You should always
use :class:`pyqode.core.api.TextBlockHelper` to retrieve or modify
those data.
"""
#: Signal emitted at the start of highlightBlock. Parameters are the
#: highlighter instance and the current text block
block_highlight_started = QtCore.Signal(object, object)
#: Signal emitted at the end of highlightBlock. Parameters are the
#: highlighter instance and the current text block
block_highlight_finished = QtCore.Signal(object, object)
@property
def formats(self):
"""
Returns the color shcme formats dict.
"""
return self._color_scheme.formats
@property
def color_scheme(self):
"""
Gets/Sets the color scheme of the syntax highlighter, this will trigger
a rehighlight automatically.
"""
return self._color_scheme
@color_scheme.setter
def color_scheme(self, color_scheme):
if isinstance(color_scheme, str):
color_scheme = ColorScheme(color_scheme)
if color_scheme.name != self._color_scheme.name:
self._color_scheme = color_scheme
self.refresh_editor(color_scheme)
self.rehighlight()
def refresh_editor(self, color_scheme):
"""
Refresh editor settings (background and highlight colors) when color
scheme changed.
:param color_scheme: new color scheme.
"""
self.editor.background = color_scheme.background
self.editor.foreground = color_scheme.formats[
'normal'].foreground().color()
self.editor.whitespaces_foreground = color_scheme.formats[
'whitespace'].foreground().color()
try:
mode = self.editor.modes.get('CaretLineHighlighterMode')
except KeyError:
pass
else:
mode.background = color_scheme.highlight
mode.refresh()
try:
mode = self.editor.panels.get('FoldingPanel')
except KeyError:
pass
else:
mode.refresh_decorations(force=True)
self.editor._reset_stylesheet()
def __init__(self, parent, color_scheme=None):
"""
:param parent: parent document (QTextDocument)
:param color_scheme: color scheme to use.
"""
QtGui.QSyntaxHighlighter.__init__(self, parent)
Mode.__init__(self)
if not color_scheme:
color_scheme = ColorScheme('qt')
self._color_scheme = color_scheme
self._spaces_ptrn = QtCore.QRegExp(r'[ \t]+')
#: Fold detector. Set it to a valid FoldDetector to get code folding
#: to work. Default is None
self.fold_detector = None
self.WHITESPACES = QtCore.QRegExp(r'\s+')
def on_state_changed(self, state):
if self._on_close:
return
if state:
self.setDocument(self.editor.document())
else:
self.setDocument(None)
def _highlight_whitespaces(self, text):
index = self.WHITESPACES.indexIn(text, 0)
while index >= 0:
index = self.WHITESPACES.pos(0)
length = len(self.WHITESPACES.cap(0))
self.setFormat(index, length, self.formats['whitespace'])
index = self.WHITESPACES.indexIn(text, index + length)
@staticmethod
def _find_prev_non_blank_block(current_block):
previous_block = (current_block.previous()
if current_block.blockNumber() else None)
# find the previous non-blank block
while (previous_block and previous_block.blockNumber() and
previous_block.text().strip() == ''):
previous_block = previous_block.previous()
return previous_block
def highlightBlock(self, text):
"""
Highlights a block of text. Please do not override, this method.
Instead you should implement
:func:`pyqode.core.api.SyntaxHighlighter.highlight_block`.
:param text: text to highlight.
"""
if not self.enabled:
return
current_block = self.currentBlock()
previous_block = self._find_prev_non_blank_block(current_block)
if self.editor:
self.highlight_block(text, current_block)
if self.editor.show_whitespaces:
self._highlight_whitespaces(text)
if self.fold_detector is not None:
self.fold_detector._editor = weakref.ref(self.editor)
self.fold_detector.process_block(
current_block, previous_block, text)
def highlight_block(self, text, block):
"""
Abstract method. Override this to apply syntax highlighting.
:param text: Line of text to highlight.
:param block: current block
"""
raise NotImplementedError()
def rehighlight(self):
"""
Rehighlight the entire document, may be slow.
"""
start = time.time()
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
try:
super(SyntaxHighlighter, self).rehighlight()
except RuntimeError:
# cloned widget, no need to rehighlight the same document twice ;)
pass
QtWidgets.QApplication.restoreOverrideCursor()
end = time.time()
_logger().debug('rehighlight duration: %fs' % (end - start))
def on_install(self, editor):
super(SyntaxHighlighter, self).on_install(editor)
self.refresh_editor(self.color_scheme)
self.document().setParent(editor)
self.setParent(editor)
def clone_settings(self, original):
self._color_scheme = original.color_scheme
class TextBlockUserData(QtGui.QTextBlockUserData):
"""
Custom text block user data, mainly used to store checker messages and
markers.
"""
def __init__(self):
super(TextBlockUserData, self).__init__()
#: List of checker messages associated with the block.
self.messages = []
#: List of markers draw by a marker panel.
self.markers = []
|