|
4 | 4 | from modulefinder import ModuleFinder |
5 | 5 |
|
6 | 6 | from .code_editor import CodeEditor |
7 | | -from .pyhighlight import PythonHighlighter |
8 | 7 |
|
9 | | -from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer, Qt, QEvent |
| 8 | +from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer, Qt, QEvent, QRegExp |
10 | 9 | from PyQt5.QtWidgets import ( |
11 | 10 | QAction, |
12 | 11 | QFileDialog, |
|
15 | 14 | QListWidgetItem, |
16 | 15 | QShortcut, |
17 | 16 | ) |
18 | | -from PyQt5.QtGui import QFontDatabase, QTextCursor, QKeyEvent |
| 17 | +from PyQt5.QtGui import ( |
| 18 | + QFontDatabase, |
| 19 | + QTextCursor, |
| 20 | + QKeyEvent, |
| 21 | + QColor, |
| 22 | + QTextCharFormat, |
| 23 | + QFont, |
| 24 | + QSyntaxHighlighter, |
| 25 | +) |
19 | 26 | from path import Path |
20 | 27 |
|
21 | 28 | import sys |
|
30 | 37 | from ..icons import icon |
31 | 38 |
|
32 | 39 |
|
| 40 | +def format(color, style=""): |
| 41 | + """ |
| 42 | + Return a QTextCharFormat with the given attributes. |
| 43 | + """ |
| 44 | + _color = QColor() |
| 45 | + _color.setNamedColor(color) |
| 46 | + |
| 47 | + _format = QTextCharFormat() |
| 48 | + _format.setForeground(_color) |
| 49 | + if "bold" in style: |
| 50 | + _format.setFontWeight(QFont.Bold) |
| 51 | + if "italic" in style: |
| 52 | + _format.setFontItalic(True) |
| 53 | + |
| 54 | + return _format |
| 55 | + |
| 56 | + |
| 57 | +# Syntax styles that can be shared by all languages |
| 58 | +STYLES = { |
| 59 | + "keyword": format("blue"), |
| 60 | + "operator": format("gray"), |
| 61 | + "brace": format("darkGray"), |
| 62 | + "defclass": format("gray", "bold"), |
| 63 | + "string": format("orange"), |
| 64 | + "string2": format("darkMagenta"), |
| 65 | + "comment": format("darkGreen", "italic"), |
| 66 | + "self": format("black", "italic"), |
| 67 | + "numbers": format("magenta"), |
| 68 | +} |
| 69 | + |
| 70 | + |
| 71 | +class PythonHighlighter(QSyntaxHighlighter): |
| 72 | + """ |
| 73 | + Syntax highlighter for the Python language. |
| 74 | + """ |
| 75 | + |
| 76 | + # Python keywords |
| 77 | + keywords = [ |
| 78 | + "and", |
| 79 | + "assert", |
| 80 | + "break", |
| 81 | + "class", |
| 82 | + "continue", |
| 83 | + "def", |
| 84 | + "del", |
| 85 | + "elif", |
| 86 | + "else", |
| 87 | + "except", |
| 88 | + "exec", |
| 89 | + "finally", |
| 90 | + "for", |
| 91 | + "from", |
| 92 | + "global", |
| 93 | + "if", |
| 94 | + "import", |
| 95 | + "in", |
| 96 | + "is", |
| 97 | + "lambda", |
| 98 | + "not", |
| 99 | + "or", |
| 100 | + "pass", |
| 101 | + "print", |
| 102 | + "raise", |
| 103 | + "return", |
| 104 | + "try", |
| 105 | + "while", |
| 106 | + "yield", |
| 107 | + "None", |
| 108 | + "True", |
| 109 | + "False", |
| 110 | + ] |
| 111 | + |
| 112 | + # Python operators |
| 113 | + operators = [ |
| 114 | + "=", |
| 115 | + # Comparison |
| 116 | + "==", |
| 117 | + "!=", |
| 118 | + "<", |
| 119 | + "<=", |
| 120 | + ">", |
| 121 | + ">=", |
| 122 | + # Arithmetic |
| 123 | + "\+", |
| 124 | + "-", |
| 125 | + "\*", |
| 126 | + "/", |
| 127 | + "//", |
| 128 | + "\%", |
| 129 | + "\*\*", |
| 130 | + # In-place |
| 131 | + "\+=", |
| 132 | + "-=", |
| 133 | + "\*=", |
| 134 | + "/=", |
| 135 | + "\%=", |
| 136 | + # Bitwise |
| 137 | + "\^", |
| 138 | + "\|", |
| 139 | + "\&", |
| 140 | + "\~", |
| 141 | + ">>", |
| 142 | + "<<", |
| 143 | + ] |
| 144 | + |
| 145 | + # Python braces |
| 146 | + braces = [ |
| 147 | + "\{", |
| 148 | + "\}", |
| 149 | + "\(", |
| 150 | + "\)", |
| 151 | + "\[", |
| 152 | + "\]", |
| 153 | + ] |
| 154 | + |
| 155 | + def __init__(self, parent=None): |
| 156 | + super(PythonHighlighter, self).__init__(parent) |
| 157 | + |
| 158 | + # Multi-line strings (expression, flag, style) |
| 159 | + self.tri_single = (QRegExp("'''"), 1, STYLES["string2"]) |
| 160 | + self.tri_double = (QRegExp('"""'), 2, STYLES["string2"]) |
| 161 | + |
| 162 | + rules = [] |
| 163 | + |
| 164 | + # Keyword, operator, and brace rules |
| 165 | + rules += [ |
| 166 | + (r"\b%s\b" % w, 0, STYLES["keyword"]) for w in PythonHighlighter.keywords |
| 167 | + ] |
| 168 | + rules += [ |
| 169 | + (r"%s" % o, 0, STYLES["operator"]) for o in PythonHighlighter.operators |
| 170 | + ] |
| 171 | + rules += [(r"%s" % b, 0, STYLES["brace"]) for b in PythonHighlighter.braces] |
| 172 | + |
| 173 | + # All other rules |
| 174 | + rules += [ |
| 175 | + # 'self' |
| 176 | + (r"\bself\b", 0, STYLES["self"]), |
| 177 | + # 'def' followed by an identifier |
| 178 | + (r"\bdef\b\s*(\w+)", 1, STYLES["defclass"]), |
| 179 | + # 'class' followed by an identifier |
| 180 | + (r"\bclass\b\s*(\w+)", 1, STYLES["defclass"]), |
| 181 | + # Numeric literals |
| 182 | + (r"\b[+-]?[0-9]+[lL]?\b", 0, STYLES["numbers"]), |
| 183 | + (r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", 0, STYLES["numbers"]), |
| 184 | + (r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b", 0, STYLES["numbers"]), |
| 185 | + # Double-quoted string, possibly containing escape sequences |
| 186 | + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES["string"]), |
| 187 | + # Single-quoted string, possibly containing escape sequences |
| 188 | + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES["string"]), |
| 189 | + # From '#' until a newline |
| 190 | + (r"#[^\n]*", 0, STYLES["comment"]), |
| 191 | + ] |
| 192 | + |
| 193 | + # Build a QRegExp for each pattern |
| 194 | + self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] |
| 195 | + |
| 196 | + def highlightBlock(self, text): |
| 197 | + """ |
| 198 | + Apply syntax highlighting to the given block of text. |
| 199 | + """ |
| 200 | + self.tripleQuoutesWithinStrings = [] |
| 201 | + # Do other syntax formatting |
| 202 | + for expression, nth, format in self.rules: |
| 203 | + index = expression.indexIn(text, 0) |
| 204 | + if index >= 0: |
| 205 | + # if there is a string we check |
| 206 | + # if there are some triple quotes within the string |
| 207 | + # they will be ignored if they are matched again |
| 208 | + if expression.pattern() in [ |
| 209 | + r'"[^"\\]*(\\.[^"\\]*)*"', |
| 210 | + r"'[^'\\]*(\\.[^'\\]*)*'", |
| 211 | + ]: |
| 212 | + innerIndex = self.tri_single[0].indexIn(text, index + 1) |
| 213 | + if innerIndex == -1: |
| 214 | + innerIndex = self.tri_double[0].indexIn(text, index + 1) |
| 215 | + |
| 216 | + if innerIndex != -1: |
| 217 | + tripleQuoteIndexes = range(innerIndex, innerIndex + 3) |
| 218 | + self.tripleQuoutesWithinStrings.extend(tripleQuoteIndexes) |
| 219 | + |
| 220 | + while index >= 0: |
| 221 | + # skipping triple quotes within strings |
| 222 | + if index in self.tripleQuoutesWithinStrings: |
| 223 | + index += 1 |
| 224 | + expression.indexIn(text, index) |
| 225 | + continue |
| 226 | + |
| 227 | + # We actually want the index of the nth match |
| 228 | + index = expression.pos(nth) |
| 229 | + length = len(expression.cap(nth)) |
| 230 | + self.setFormat(index, length, format) |
| 231 | + index = expression.indexIn(text, index + length) |
| 232 | + |
| 233 | + self.setCurrentBlockState(0) |
| 234 | + |
| 235 | + # Do multi-line strings |
| 236 | + in_multiline = self.match_multiline(text, *self.tri_single) |
| 237 | + if not in_multiline: |
| 238 | + in_multiline = self.match_multiline(text, *self.tri_double) |
| 239 | + |
| 240 | + def match_multiline(self, text, delimiter, in_state, style): |
| 241 | + """ |
| 242 | + Do highlighting of multi-line strings. ``delimiter`` should be a |
| 243 | + ``QRegExp`` for triple-single-quotes or triple-double-quotes, and |
| 244 | + ``in_state`` should be a unique integer to represent the corresponding |
| 245 | + state changes when inside those strings. Returns True if we're still |
| 246 | + inside a multi-line string when this function is finished. |
| 247 | + """ |
| 248 | + # If inside triple-single quotes, start at 0 |
| 249 | + if self.previousBlockState() == in_state: |
| 250 | + start = 0 |
| 251 | + add = 0 |
| 252 | + # Otherwise, look for the delimiter on this line |
| 253 | + else: |
| 254 | + start = delimiter.indexIn(text) |
| 255 | + # skipping triple quotes within strings |
| 256 | + if start in self.tripleQuoutesWithinStrings: |
| 257 | + return False |
| 258 | + # Move past this match |
| 259 | + add = delimiter.matchedLength() |
| 260 | + |
| 261 | + # As long as there's a delimiter match on this line... |
| 262 | + while start >= 0: |
| 263 | + # Look for the ending delimiter |
| 264 | + end = delimiter.indexIn(text, start + add) |
| 265 | + # Ending delimiter on this line? |
| 266 | + if end >= add: |
| 267 | + length = end - start + add + delimiter.matchedLength() |
| 268 | + self.setCurrentBlockState(0) |
| 269 | + # No; multi-line string |
| 270 | + else: |
| 271 | + self.setCurrentBlockState(in_state) |
| 272 | + length = len(text) - start + add |
| 273 | + # Apply formatting |
| 274 | + self.setFormat(start, length, style) |
| 275 | + # Look for the next match |
| 276 | + start = delimiter.indexIn(text, start + length) |
| 277 | + |
| 278 | + # Return True if still inside a multi-line string, False otherwise |
| 279 | + if self.currentBlockState() == in_state: |
| 280 | + return True |
| 281 | + else: |
| 282 | + return False |
| 283 | + |
| 284 | + |
33 | 285 | class EditorDebugger: |
34 | 286 | def __init__(self): |
35 | 287 | self.breakpoints = [] |
|
0 commit comments