|
| 1 | +#!/usr/bin/env python2 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +''' |
| 4 | +Licensed under the terms of the MIT License |
| 5 | +https://github.com/luchko/QCodeEditor |
| 6 | +@author: Ivan Luchko ([email protected]) |
| 7 | +
|
| 8 | +Python Highlighting added by: |
| 9 | +https://github.com/unihernandez22/QCodeEditor |
| 10 | +@author: unihernandez22 |
| 11 | +
|
| 12 | +Adapted to Binary Ninja by: |
| 13 | +@author: Jordan Wiens (https://github.com/psifertex) |
| 14 | +
|
| 15 | +Integrating syntax highlighting from: |
| 16 | +https://wiki.python.org/moin/PyQt/Python%20syntax%20highlighting |
| 17 | +Released under the Modified BSD License: http://directory.fsf.org/wiki/License:BSD_3Clause |
| 18 | +
|
| 19 | +Note that this will not be merged back to the parent repositories as it's been |
| 20 | +modified to be heavily dependent on the BN theme system. |
| 21 | +''' |
| 22 | + |
| 23 | +from PySide2.QtCore import Qt, QRect, QRegExp |
| 24 | +from PySide2.QtWidgets import QWidget, QTextEdit, QPlainTextEdit |
| 25 | +from PySide2.QtGui import (QPainter, QFont, QSyntaxHighlighter, QTextFormat, QTextCharFormat) |
| 26 | +from binaryninjaui import (getMonospaceFont, getThemeColor, ThemeColor) |
| 27 | + |
| 28 | + |
| 29 | +def format(color, style=''): |
| 30 | + """Return a QTextCharFormat with the given attributes.""" |
| 31 | + _color = eval('getThemeColor(ThemeColor.%s)' % color) |
| 32 | + |
| 33 | + _format = QTextCharFormat() |
| 34 | + _format.setForeground(_color) |
| 35 | + if 'bold' in style: |
| 36 | + _format.setFontWeight(QFont.Bold) |
| 37 | + if 'italic' in style: |
| 38 | + _format.setFontItalic(True) |
| 39 | + |
| 40 | + return _format |
| 41 | + |
| 42 | +STYLES = { |
| 43 | + 'keyword': format('StackVariableColor'), |
| 44 | + 'operator': format('TokenHighlightColor'), |
| 45 | + 'brace': format('LinearDisassemblySeparatorColor'), |
| 46 | + 'defclass': format('DataSymbolColor'), |
| 47 | + 'string': format('StringColor'), |
| 48 | + 'string2': format('TypeNameColor'), |
| 49 | + 'comment': format('AnnotationColor', 'italic'), |
| 50 | + 'self': format('KeywordColor', 'italic'), |
| 51 | + 'numbers': format('NumberColor'), |
| 52 | + 'numberbar': getThemeColor(ThemeColor.BackgroundHighlightDarkColor), |
| 53 | + 'blockselected': getThemeColor(ThemeColor.TokenHighlightColor), |
| 54 | + 'blocknormal': getThemeColor(ThemeColor.TokenSelectionColor) |
| 55 | +} |
| 56 | + |
| 57 | +class PythonHighlighter (QSyntaxHighlighter): |
| 58 | + """Syntax highlighter for the Python language. |
| 59 | + """ |
| 60 | + # Python keywords |
| 61 | + keywords = [ |
| 62 | + 'and', 'assert', 'break', 'class', 'continue', 'def', |
| 63 | + 'del', 'elif', 'else', 'except', 'exec', 'finally', |
| 64 | + 'for', 'from', 'global', 'if', 'import', 'in', |
| 65 | + 'is', 'lambda', 'not', 'or', 'pass', 'print', |
| 66 | + 'raise', 'return', 'try', 'while', 'yield', |
| 67 | + 'None', 'True', 'False', |
| 68 | + ] |
| 69 | + |
| 70 | + # Python operators |
| 71 | + operators = [ |
| 72 | + '=', |
| 73 | + # Comparison |
| 74 | + '==', '!=', '<', '<=', '>', '>=', |
| 75 | + # Arithmetic |
| 76 | + '\+', '-', '\*', '/', '//', '\%', '\*\*', |
| 77 | + # In-place |
| 78 | + '\+=', '-=', '\*=', '/=', '\%=', |
| 79 | + # Bitwise |
| 80 | + '\^', '\|', '\&', '\~', '>>', '<<', |
| 81 | + ] |
| 82 | + |
| 83 | + # Python braces |
| 84 | + braces = [ |
| 85 | + '\{', '\}', '\(', '\)', '\[', '\]', |
| 86 | + ] |
| 87 | + def __init__(self, document): |
| 88 | + QSyntaxHighlighter.__init__(self, document) |
| 89 | + |
| 90 | + # Multi-line strings (expression, flag, style) |
| 91 | + # FIXME: The triple-quotes in these two lines will mess up the |
| 92 | + # syntax highlighting from this point onward |
| 93 | + self.tri_single = (QRegExp("'''"), 1, STYLES['string2']) |
| 94 | + self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) |
| 95 | + |
| 96 | + rules = [] |
| 97 | + |
| 98 | + # Keyword, operator, and brace rules |
| 99 | + rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) |
| 100 | + for w in PythonHighlighter.keywords] |
| 101 | + rules += [(r'%s' % o, 0, STYLES['operator']) |
| 102 | + for o in PythonHighlighter.operators] |
| 103 | + rules += [(r'%s' % b, 0, STYLES['brace']) |
| 104 | + for b in PythonHighlighter.braces] |
| 105 | + |
| 106 | + # All other rules |
| 107 | + rules += [ |
| 108 | + # 'self' |
| 109 | + (r'\bself\b', 0, STYLES['self']), |
| 110 | + |
| 111 | + # Double-quoted string, possibly containing escape sequences |
| 112 | + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), |
| 113 | + # Single-quoted string, possibly containing escape sequences |
| 114 | + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), |
| 115 | + |
| 116 | + # 'def' followed by an identifier |
| 117 | + (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), |
| 118 | + # 'class' followed by an identifier |
| 119 | + (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), |
| 120 | + |
| 121 | + # From '#' until a newline |
| 122 | + (r'#[^\n]*', 0, STYLES['comment']), |
| 123 | + |
| 124 | + # Numeric literals |
| 125 | + (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), |
| 126 | + (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), |
| 127 | + (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), |
| 128 | + ] |
| 129 | + |
| 130 | + # Build a QRegExp for each pattern |
| 131 | + self.rules = [(QRegExp(pat), index, fmt) |
| 132 | + for (pat, index, fmt) in rules] |
| 133 | + |
| 134 | + |
| 135 | + def highlightBlock(self, text): |
| 136 | + """Apply syntax highlighting to the given block of text. |
| 137 | + """ |
| 138 | + # Do other syntax formatting |
| 139 | + for expression, nth, format in self.rules: |
| 140 | + index = expression.indexIn(text, 0) |
| 141 | + |
| 142 | + while index >= 0: |
| 143 | + # We actually want the index of the nth match |
| 144 | + index = expression.pos(nth) |
| 145 | + length = len(expression.cap(nth)) |
| 146 | + self.setFormat(index, length, format) |
| 147 | + index = expression.indexIn(text, index + length) |
| 148 | + |
| 149 | + self.setCurrentBlockState(0) |
| 150 | + |
| 151 | + # Do multi-line strings |
| 152 | + in_multiline = self.match_multiline(text, *self.tri_single) |
| 153 | + if not in_multiline: |
| 154 | + in_multiline = self.match_multiline(text, *self.tri_double) |
| 155 | + |
| 156 | + |
| 157 | + def match_multiline(self, text, delimiter, in_state, style): |
| 158 | + """Do highlighting of multi-line strings. ``delimiter`` should be a |
| 159 | + ``QRegExp`` for triple-single-quotes or triple-double-quotes, and |
| 160 | + ``in_state`` should be a unique integer to represent the corresponding |
| 161 | + state changes when inside those strings. Returns True if we're still |
| 162 | + inside a multi-line string when this function is finished. |
| 163 | + """ |
| 164 | + # If inside triple-single quotes, start at 0 |
| 165 | + if self.previousBlockState() == in_state: |
| 166 | + start = 0 |
| 167 | + add = 0 |
| 168 | + # Otherwise, look for the delimiter on this line |
| 169 | + else: |
| 170 | + start = delimiter.indexIn(text) |
| 171 | + # Move past this match |
| 172 | + add = delimiter.matchedLength() |
| 173 | + |
| 174 | + # As long as there's a delimiter match on this line... |
| 175 | + while start >= 0: |
| 176 | + # Look for the ending delimiter |
| 177 | + end = delimiter.indexIn(text, start + add) |
| 178 | + # Ending delimiter on this line? |
| 179 | + if end >= add: |
| 180 | + length = end - start + add + delimiter.matchedLength() |
| 181 | + self.setCurrentBlockState(0) |
| 182 | + # No; multi-line string |
| 183 | + else: |
| 184 | + self.setCurrentBlockState(in_state) |
| 185 | + length = len(text) - start + add |
| 186 | + # Apply formatting |
| 187 | + self.setFormat(start, length, style) |
| 188 | + # Look for the next match |
| 189 | + start = delimiter.indexIn(text, start + length) |
| 190 | + |
| 191 | + # Return True if still inside a multi-line string, False otherwise |
| 192 | + if self.currentBlockState() == in_state: |
| 193 | + return True |
| 194 | + else: |
| 195 | + return False |
| 196 | + |
| 197 | + |
| 198 | +class QCodeEditor(QPlainTextEdit): |
| 199 | + ''' |
| 200 | + QCodeEditor inherited from QPlainTextEdit providing: |
| 201 | +
|
| 202 | + numberBar - set by DISPLAY_LINE_NUMBERS flag equals True |
| 203 | + curent line highligthing - set by HIGHLIGHT_CURRENT_LINE flag equals True |
| 204 | + setting up QSyntaxHighlighter |
| 205 | +
|
| 206 | + references: |
| 207 | + https://john.nachtimwald.com/2009/08/19/better-qplaintextedit-with-line-numbers/ |
| 208 | + http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html |
| 209 | +
|
| 210 | + ''' |
| 211 | + class NumberBar(QWidget): |
| 212 | + '''class that deifnes textEditor numberBar''' |
| 213 | + |
| 214 | + def __init__(self, editor): |
| 215 | + QWidget.__init__(self, editor) |
| 216 | + |
| 217 | + self.editor = editor |
| 218 | + self.editor.blockCountChanged.connect(self.updateWidth) |
| 219 | + self.editor.updateRequest.connect(self.updateContents) |
| 220 | + self.font = QFont() |
| 221 | + self.numberBarColor = STYLES["numberbar"] |
| 222 | + |
| 223 | + def paintEvent(self, event): |
| 224 | + |
| 225 | + painter = QPainter(self) |
| 226 | + painter.fillRect(event.rect(), self.numberBarColor) |
| 227 | + |
| 228 | + block = self.editor.firstVisibleBlock() |
| 229 | + |
| 230 | + # Iterate over all visible text blocks in the document. |
| 231 | + while block.isValid(): |
| 232 | + blockNumber = block.blockNumber() |
| 233 | + block_top = self.editor.blockBoundingGeometry(block).translated(self.editor.contentOffset()).top() |
| 234 | + |
| 235 | + # Check if the position of the block is out side of the visible area. |
| 236 | + if not block.isVisible() or block_top >= event.rect().bottom(): |
| 237 | + break |
| 238 | + |
| 239 | + # We want the line number for the selected line to be bold. |
| 240 | + if blockNumber == self.editor.textCursor().blockNumber(): |
| 241 | + self.font.setBold(True) |
| 242 | + painter.setPen(STYLES["blockselected"]) |
| 243 | + else: |
| 244 | + self.font.setBold(False) |
| 245 | + painter.setPen(STYLES["blocknormal"]) |
| 246 | + painter.setFont(self.font) |
| 247 | + |
| 248 | + # Draw the line number right justified at the position of the line. |
| 249 | + paint_rect = QRect(0, block_top, self.width(), self.editor.fontMetrics().height()) |
| 250 | + painter.drawText(paint_rect, Qt.AlignLeft, str(blockNumber+1)) |
| 251 | + |
| 252 | + block = block.next() |
| 253 | + |
| 254 | + painter.end() |
| 255 | + |
| 256 | + QWidget.paintEvent(self, event) |
| 257 | + |
| 258 | + def getWidth(self): |
| 259 | + count = self.editor.blockCount() |
| 260 | + width = self.fontMetrics().width(str(count)) + 10 |
| 261 | + return width |
| 262 | + |
| 263 | + def updateWidth(self): |
| 264 | + width = self.getWidth() |
| 265 | + if self.width() != width: |
| 266 | + self.setFixedWidth(width) |
| 267 | + self.editor.setViewportMargins(width, 0, 0, 0); |
| 268 | + |
| 269 | + def updateContents(self, rect, scroll): |
| 270 | + if scroll: |
| 271 | + self.scroll(0, scroll) |
| 272 | + else: |
| 273 | + self.update(0, rect.y(), self.width(), rect.height()) |
| 274 | + |
| 275 | + if rect.contains(self.editor.viewport().rect()): |
| 276 | + fontSize = self.editor.currentCharFormat().font().pointSize() |
| 277 | + self.font.setPointSize(fontSize) |
| 278 | + self.font.setStyle(QFont.StyleNormal) |
| 279 | + self.updateWidth() |
| 280 | + |
| 281 | + |
| 282 | + def __init__(self, DISPLAY_LINE_NUMBERS=True, HIGHLIGHT_CURRENT_LINE=True, |
| 283 | + SyntaxHighlighter=None, *args): |
| 284 | + ''' |
| 285 | + Parameters |
| 286 | + ---------- |
| 287 | + DISPLAY_LINE_NUMBERS : bool |
| 288 | + switch on/off the presence of the lines number bar |
| 289 | + HIGHLIGHT_CURRENT_LINE : bool |
| 290 | + switch on/off the current line highliting |
| 291 | + SyntaxHighlighter : QSyntaxHighlighter |
| 292 | + should be inherited from QSyntaxHighlighter |
| 293 | +
|
| 294 | + ''' |
| 295 | + super(QCodeEditor, self).__init__() |
| 296 | + |
| 297 | + self.setFont(QFont("Ubuntu Mono", 11)) |
| 298 | + self.setLineWrapMode(QPlainTextEdit.NoWrap) |
| 299 | + |
| 300 | + self.DISPLAY_LINE_NUMBERS = DISPLAY_LINE_NUMBERS |
| 301 | + |
| 302 | + if DISPLAY_LINE_NUMBERS: |
| 303 | + self.number_bar = self.NumberBar(self) |
| 304 | + |
| 305 | + if HIGHLIGHT_CURRENT_LINE: |
| 306 | + self.currentLineNumber = None |
| 307 | + self.currentLineColor = STYLES['currentLine'] |
| 308 | + self.cursorPositionChanged.connect(self.highligtCurrentLine) |
| 309 | + |
| 310 | + if SyntaxHighlighter is not None: # add highlighter to textdocument |
| 311 | + self.highlighter = SyntaxHighlighter(self.document()) |
| 312 | + |
| 313 | + def resizeEvent(self, *e): |
| 314 | + '''overload resizeEvent handler''' |
| 315 | + |
| 316 | + if self.DISPLAY_LINE_NUMBERS: # resize number_bar widget |
| 317 | + cr = self.contentsRect() |
| 318 | + rec = QRect(cr.left(), cr.top(), self.number_bar.getWidth(), cr.height()) |
| 319 | + self.number_bar.setGeometry(rec) |
| 320 | + |
| 321 | + QPlainTextEdit.resizeEvent(self, *e) |
| 322 | + |
| 323 | + def highligtCurrentLine(self): |
| 324 | + newCurrentLineNumber = self.textCursor().blockNumber() |
| 325 | + if newCurrentLineNumber != self.currentLineNumber: |
| 326 | + self.currentLineNumber = newCurrentLineNumber |
| 327 | + hi_selection = QTextEdit.ExtraSelection() |
| 328 | + hi_selection.format.setBackground(self.currentLineColor) |
| 329 | + hi_selection.format.setProperty(QTextFormat.FullWidthSelection, True) |
| 330 | + hi_selection.cursor = self.textCursor() |
| 331 | + hi_selection.cursor.clearSelection() |
| 332 | + self.setExtraSelections([hi_selection]) |
| 333 | + |
| 334 | +############################################################################## |
| 335 | + |
| 336 | +if __name__ == '__main__': |
| 337 | + |
| 338 | + # TESTING |
| 339 | + |
| 340 | + def run_test(): |
| 341 | + |
| 342 | + from PySide2.QtGui import QApplication |
| 343 | + import sys |
| 344 | + |
| 345 | + app = QApplication([]) |
| 346 | + |
| 347 | + editor = QCodeEditor(DISPLAY_LINE_NUMBERS=True, |
| 348 | + HIGHLIGHT_CURRENT_LINE=True, |
| 349 | + SyntaxHighlighter=PythonHighlighter) |
| 350 | + |
| 351 | +# text = '''<FINITELATTICE> |
| 352 | +# <LATTICE name="myLattice"> |
| 353 | +# <BASIS> |
| 354 | +# <VECTOR>1.0 0.0 0.0</VECTOR> |
| 355 | +# <VECTOR>0.0 1.0 0.0</VECTOR> |
| 356 | +# </BASIS> |
| 357 | +# </LATTICE> |
| 358 | +# <PARAMETER name="L" /> |
| 359 | +# <PARAMETER default="L" name="W" /> |
| 360 | +# <EXTENT dimension="1" size="L" /> |
| 361 | +# <EXTENT dimension="2" size="W" /> |
| 362 | +# <BOUNDARY type="periodic" /> |
| 363 | +# </FINITELATTICE> |
| 364 | +# ''' |
| 365 | + text = """\ |
| 366 | +def hello(text): |
| 367 | + print(text) |
| 368 | +
|
| 369 | +hello('Hello World') |
| 370 | +
|
| 371 | +# Comment""" |
| 372 | + editor.setPlainText(text) |
| 373 | + editor.resize(400,250) |
| 374 | + editor.show() |
| 375 | + |
| 376 | + sys.exit(app.exec_()) |
| 377 | + |
| 378 | + |
| 379 | + run_test() |
0 commit comments