Skip to content

Commit 3ca43e2

Browse files
committed
Optimize and improve code highlighting
Define formats and regex patterns, once, up front. For patterns with multiple strings (ie keyword) use a single pipe pattern rather than repeatedly iterate over each keyword, redefining the pattern each time.
1 parent fa8834b commit 3ca43e2

File tree

4 files changed

+239
-115
lines changed

4 files changed

+239
-115
lines changed

preditor/gui/codehighlighter.py

Lines changed: 207 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22

33
import json
4+
import keyword
45
import os
56
import re
67

@@ -10,103 +11,191 @@
1011

1112

1213
class CodeHighlighter(QSyntaxHighlighter):
13-
def __init__(self, widget):
14+
15+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
16+
# # # INITIALIZATION # # #
17+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
18+
19+
def __init__(self, widget, language):
1420
super(CodeHighlighter, self).__init__(widget)
21+
self._consoleMode = False
22+
23+
self.initHighlightVariables()
24+
self.setLanguage(language)
25+
26+
self.defineHighlightVariables()
27+
28+
def initHighlightVariables(self):
29+
"""Initialize the variables which will be used in code highlighting"""
30+
31+
# For each call of highlightBlock, keep track of the spans of each highlight, so
32+
# we can prevent overlapping spans (ie if a string is found, but it's within a
33+
# comment, do not highlight it).
34+
self.spans = []
1535

16-
# setup the search rules
36+
# Language specific lists
37+
self._comments = []
1738
self._keywords = []
1839
self._strings = []
19-
self._comments = []
20-
self._consoleMode = False
21-
# color storage
40+
41+
# Patterns
42+
self._commentPattern = None
43+
self._keywordPattern = None
44+
self._resultPattern = None
45+
self._stringsPattern = None
46+
47+
# Formats
48+
self._commentFormat = None
49+
self._keywordFormat = None
50+
self._resultFormat = None
51+
self._stringFormat = None
52+
53+
# Colors. These may be overriden by parent colors, which themselves my be
54+
# overridden by stylesheets (ie Bright.css)
2255
self._commentColor = QColor(0, 206, 52)
23-
self._keywordColor = QColor(17, 154, 255)
24-
self._stringColor = QColor(255, 128, 0)
56+
self._keywordColor = QColor(255, 0, 255)
2557
self._resultColor = QColor(125, 128, 128)
58+
self._stringColor = QColor(255, 128, 0)
2659

27-
# setup the font
28-
font = widget.font()
29-
font.setFamily('Courier New')
30-
widget.setFont(font)
31-
32-
def commentColor(self):
33-
# pull the color from the parent if possible because this doesn't support
34-
# stylesheets
35-
parent = self.parent()
36-
if parent and hasattr(parent, 'commentColor'):
37-
return parent.commentColor
38-
return self._commentColor
60+
def setLanguage(self, lang):
61+
"""Sets the language of the highlighter by loading the json definition"""
62+
filename = resourcePath('lang/%s.json' % lang.lower())
63+
if os.path.exists(filename):
64+
data = json.load(open(filename))
65+
self.setObjectName(data.get('name', ''))
3966

40-
def setCommentColor(self, color):
41-
# set the color for the parent if possible because this doesn't support
42-
# stylesheets
43-
parent = self.parent()
44-
if parent and hasattr(parent, 'commentColor'):
45-
parent.commentColor = color
46-
self._commentColor = color
67+
self._comments = data.get('comments', [])
68+
self._strings = data.get('strings', [])
4769

48-
def commentFormat(self):
49-
"""returns the comments QTextCharFormat for this highlighter"""
50-
format = QTextCharFormat()
51-
format.setForeground(self.commentColor())
52-
format.setFontItalic(True)
70+
# If using python, we can get keywords dynamically, otherwise get them from
71+
# the language json data.
72+
if lang.lower() == "python":
73+
self._keywords = keyword.kwlist
74+
else:
75+
self._keywords = data.get('keywords', [])
5376

54-
return format
77+
return True
78+
return False
5579

56-
def isConsoleMode(self):
57-
"""checks to see if this highlighter is in console mode"""
58-
return self._consoleMode
80+
def defineHighlightVariables(self):
81+
"""Define the formats and regex patterns which will be used to highlight
82+
code."""
83+
# Define highlight formats
84+
self.defineCommentFormat()
85+
self.defineKeywordFormat()
86+
self.defineResultFormat()
87+
self.defineStringFormat()
88+
89+
# Define highlight regex patterns
90+
self.defineCommentPattern()
91+
self.defineKeywordPattern()
92+
self.defineResultPattern()
93+
self.defineStringPattern()
94+
95+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
96+
# # # PROCESSING # # #
97+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
5998

6099
def highlightBlock(self, text):
61-
"""highlights the inputed text block based on the rules of this code
100+
"""Highlights the inputed text block based on the rules of this code
62101
highlighter"""
63-
if not self.isConsoleMode() or str(text).startswith('>>>'):
64-
# format the result lines
65-
format = self.resultFormat()
66-
parent = self.parent()
67-
if parent and hasattr(parent, 'outputPrompt'):
68-
self.highlightText(
69-
text,
70-
re.compile('%s[^\\n]*' % re.escape(parent.outputPrompt())),
71-
format,
72-
)
73102

74-
# format the keywords
75-
format = self.keywordFormat()
76-
for kwd in self._keywords:
77-
self.highlightText(text, re.compile(r'\b%s\b' % kwd), format)
103+
# Reset the highlight spans for this text block
104+
self.spans = []
78105

79-
# format the strings
80-
format = self.stringFormat()
106+
if not self.isConsoleMode() or str(text).startswith('>>>'):
81107

82-
for string in self._strings:
108+
# We only have a result pattern if the parent has an attr "outputPrompt", so
109+
# only proceed if we have been able to define self._resultPattern.
110+
if self._resultPattern:
83111
self.highlightText(
84112
text,
85-
re.compile('{s}[^{s}]*{s}'.format(s=string)),
86-
format,
113+
self._resultPattern,
114+
self._resultFormat,
87115
)
88116

89-
# format the comments
90-
format = self.commentFormat()
91-
for comment in self._comments:
92-
self.highlightText(text, re.compile(comment), format)
117+
# Format the strings
118+
self.highlightText(
119+
text,
120+
self._stringPattern,
121+
self._stringFormat,
122+
)
93123

94-
def highlightText(self, text, expr, format, offset=0, includeLast=False):
124+
# format the comments
125+
self.highlightText(
126+
text,
127+
self._commentPattern,
128+
self._commentFormat,
129+
)
130+
131+
# Format the keywords
132+
self.highlightText(
133+
text,
134+
self._keywordPattern,
135+
self._keywordFormat,
136+
)
137+
138+
def highlightText(self, text, expr, format):
95139
"""Highlights a text group with an expression and format
96140
97141
Args:
98142
text (str): text to highlight
99143
expr (QRegularExpression): search parameter
100144
format (QTextCharFormat): formatting rule
101-
includeLast (bool): whether or not the last character should be highlighted
102145
"""
146+
if expr is None or not text:
147+
return
148+
103149
# highlight all the given matches to the expression in the text
104150
for match in expr.finditer(text):
105-
start, end = match.span()
151+
match_span = match.span()
152+
start, end = match_span
106153
length = end - start
107-
if includeLast:
108-
length += 1
109-
self.setFormat(start, length, format)
154+
155+
# Determine if the current highlight is within an already determined
156+
# highlight, if so, let's block it.
157+
blocked = False
158+
for span in self.spans:
159+
if start > span[0] and start < span[-1]:
160+
blocked = True
161+
break
162+
163+
if not blocked:
164+
self.setFormat(start, length, format)
165+
166+
# Append the current span to self.spans, so we can later block
167+
# new highlights which should be blocked
168+
self.spans.append(match_span)
169+
170+
def isConsoleMode(self):
171+
"""checks to see if this highlighter is in console mode"""
172+
return self._consoleMode
173+
174+
def setConsoleMode(self, state=False):
175+
"""sets the highlighter to only apply to console strings
176+
(lines starting with >>>)
177+
"""
178+
self._consoleMode = state
179+
180+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
181+
# # # COLORS # # #
182+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
183+
184+
def commentColor(self):
185+
# Pull the color from the parent if possible because this doesn't support
186+
# stylesheets
187+
parent = self.parent()
188+
if parent and hasattr(parent, 'commentColor'):
189+
return parent.commentColor
190+
return self._commentColor
191+
192+
def setCommentColor(self, color):
193+
# set the color for the parent if possible because this doesn't support
194+
# stylesheets
195+
parent = self.parent()
196+
if parent and hasattr(parent, 'commentColor'):
197+
parent.commentColor = color
198+
self._commentColor = color
110199

111200
def keywordColor(self):
112201
# pull the color from the parent if possible because this doesn't support
@@ -124,13 +213,6 @@ def setKeywordColor(self, color):
124213
parent.keywordColor = color
125214
self._keywordColor = color
126215

127-
def keywordFormat(self):
128-
"""returns the keywords QTextCharFormat for this highlighter"""
129-
format = QTextCharFormat()
130-
format.setForeground(self.keywordColor())
131-
132-
return format
133-
134216
def resultColor(self):
135217
# pull the color from the parent if possible because this doesn't support
136218
# stylesheets
@@ -147,31 +229,6 @@ def setResultColor(self, color):
147229
parent.resultColor = color
148230
self._resultColor = color
149231

150-
def resultFormat(self):
151-
"""returns the result QTextCharFormat for this highlighter"""
152-
fmt = QTextCharFormat()
153-
fmt.setForeground(self.resultColor())
154-
return fmt
155-
156-
def setConsoleMode(self, state=False):
157-
"""sets the highlighter to only apply to console strings
158-
(lines starting with >>>)
159-
"""
160-
self._consoleMode = state
161-
162-
def setLanguage(self, lang):
163-
"""sets the language of the highlighter by loading the json definition"""
164-
filename = resourcePath('lang/%s.json' % lang.lower())
165-
if os.path.exists(filename):
166-
data = json.load(open(filename))
167-
self.setObjectName(data.get('name', ''))
168-
self._keywords = data.get('keywords', [])
169-
self._comments = data.get('comments', [])
170-
self._strings = data.get('strings', [])
171-
172-
return True
173-
return False
174-
175232
def stringColor(self):
176233
# pull the color from the parent if possible because this doesn't support
177234
# stylesheets
@@ -188,8 +245,56 @@ def setStringColor(self, color):
188245
parent.stringColor = color
189246
self._stringColor = color
190247

191-
def stringFormat(self):
192-
"""returns the keywords QTextCharFormat for this highligter"""
193-
format = QTextCharFormat()
194-
format.setForeground(self.stringColor())
195-
return format
248+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
249+
# # # Formats # # #
250+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
251+
252+
def defineCommentFormat(self):
253+
"""Define the comment format based on the comment color"""
254+
self._commentFormat = QTextCharFormat()
255+
self._commentFormat.setForeground(self.commentColor())
256+
self._commentFormat.setFontItalic(True)
257+
258+
def defineKeywordFormat(self):
259+
"""Define the keyword format based on the keyword color"""
260+
self._keywordFormat = QTextCharFormat()
261+
self._keywordFormat.setForeground(self.keywordColor())
262+
263+
def defineResultFormat(self):
264+
"""Define the result format based on the result color"""
265+
self._resultFormat = QTextCharFormat()
266+
self._resultFormat.setForeground(self.resultColor())
267+
268+
def defineStringFormat(self):
269+
"""Define the string format based on the string color"""
270+
self._stringFormat = QTextCharFormat()
271+
self._stringFormat.setForeground(self.stringColor())
272+
273+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
274+
# # # PATTERNS # # #
275+
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
276+
277+
def defineCommentPattern(self):
278+
"""Define the regex pattern to use for comment"""
279+
pattern = "|".join(self._comments)
280+
self._commentPattern = re.compile(pattern)
281+
282+
def defineKeywordPattern(self):
283+
"""Define the regex pattern to use for keyword"""
284+
keywords = [r"\b{}\b".format(word) for word in self._keywords]
285+
pattern = "|".join(keywords)
286+
self._keywordPattern = re.compile(pattern)
287+
288+
def defineResultPattern(self):
289+
"""Define the regex pattern to use for results"""
290+
parent = self.parent()
291+
if parent and hasattr(parent, 'outputPrompt'):
292+
prompt = parent.outputPrompt()
293+
pattern = '{}[^\n]*'.format(prompt)
294+
self._resultPattern = re.compile(pattern)
295+
296+
def defineStringPattern(self):
297+
"""Define the regex pattern to use for strings."""
298+
lst = ["""{0}[^{0}\n]+{0}""".format(st) for st in self._strings]
299+
pattern = "|".join(lst)
300+
self._stringPattern = re.compile(pattern)

0 commit comments

Comments
 (0)