Menu

[8f66b6]: / widgets / SyntaxHighlighter.py  Maximize  Restore  History

Download this file

239 lines (200 with data), 9.9 kB

  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
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020 Oliver Tengler
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from typing import Tuple, List, Optional, Pattern
import bisect
import re
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCharFormat, QFont, QBrush, QColor
from .IStringMatcher import IStringMatcher
class HighlightingRules:
def __init__(self, font: QFont) -> None:
self.rules: List[Tuple[Pattern, QTextCharFormat]] = []
self.lineComment: Optional[Pattern] = None
self.multiCommentStart: Optional[Pattern] = None
self.multiCommentStop: Optional[Pattern] = None
self.commentFormat: Optional[QTextCharFormat] = None
self.font = font
self.color = None
self.defaultFormat = None
def addKeywords (self, keywords: str, fontWeight: int, foreground: QBrush) -> None:
"""Adds a list of comma separated keywords."""
keywords = keywords.strip()
kwList = keywords.split(",")
# We build a single expression which matches all keywords
expr = "|".join(("\\b" + kw + "\\b" for kw in kwList))
self.addRule (expr, fontWeight, foreground)
def addCommentRule (self, singleLine: str, multiLineStart: str, multiLineEnd: str, fontWeight: int, foreground: QBrush) -> None:
"""Adds comment rules. Each parameter is a regular expression string. The multi line parameter are optional and can be empty."""
self.commentFormat = self.__createFormat(fontWeight, foreground)
self.lineComment = re.compile(singleLine)
if multiLineStart and multiLineEnd:
self.multiCommentStart = re.compile(multiLineStart)
self.multiCommentStop = re.compile(multiLineEnd)
def addRule (self, expr: str, fontWeight: int, foreground: QBrush) -> None:
"""Adds an arbitrary highlighting rule."""
fmt = self.__createFormat(fontWeight, foreground)
self.__addRule (expr, fmt)
def setColor(self, color: QColor) -> None:
self.color = color
fmt = QTextCharFormat()
fmt.setFont(self.font)
fmt.setForeground(self.color)
self.defaultFormat = fmt
def setFont (self, font: QFont) -> None:
"""Needed to change the font after the HighlightingRules object has been created."""
for rule in self.rules:
rule[1].setFont(font)
if self.commentFormat:
self.commentFormat.setFont(font)
if self.defaultFormat:
self.defaultFormat.setFont(font)
def __addRule (self, expr: str, fmt: QTextCharFormat) -> None:
self.rules.append((re.compile(expr), fmt))
def __createFormat (self, fontWeight:int, foreground: QBrush) -> QTextCharFormat:
fmt = QTextCharFormat()
fmt.setFont(self.font)
fmt.setFontWeight(fontWeight)
fmt.setForeground(foreground)
return fmt
class CommentRange:
def __init__(self, index: int, length: int=0) -> None:
self.index = index
self.length = length
def __lt__ (self, other: 'CommentRange') -> bool:
return self.index < other.index
class SyntaxHighlighter:
matchBackgroundColor = Qt.yellow
matchForegroundColor = Qt.black
match2BackgroundColor = QColor(194, 217, 255)
match2ForegroundColor = Qt.black
def __init__(self) -> None:
# The current rules
self.highlightingRules: Optional[HighlightingRules] = None
self.searchStringFormats = [QTextCharFormat(), QTextCharFormat()]
self.searchStringFormats[0].setBackground(SyntaxHighlighter.matchBackgroundColor)
self.searchStringFormats[0].setForeground(SyntaxHighlighter.matchForegroundColor)
self.searchStringFormats[1].setBackground(SyntaxHighlighter.match2BackgroundColor)
self.searchStringFormats[1].setForeground(SyntaxHighlighter.match2ForegroundColor)
self.comments: List[CommentRange] = []
self.searchDatas: List[Optional[IStringMatcher]] = [None, None]
def setFont (self, font: QFont) -> None:
if self.highlightingRules:
self.highlightingRules.setFont (font)
for strFormat in self.searchStringFormats:
strFormat.setFont (font)
def setHighlightingRules (self, rules: HighlightingRules) -> None:
self.highlightingRules = rules
for strFormat in self.searchStringFormats:
strFormat.setFont(rules.font)
strFormat.setFontWeight(QFont.Bold)
# Find all comments in the document and store them as CommentRange objects in self.comments
def setText(self, text: str) -> None:
if not self.highlightingRules:
self.comments = []
return
comments: List[CommentRange] = []
# Collect all single line comments
if self.highlightingRules.lineComment:
regLine = self.highlightingRules.lineComment
end = 0
while True:
beginMatch = regLine.search(text, end)
if not beginMatch:
break
start,end = beginMatch.span()
comments.append (CommentRange(start, end - start))
self.comments = comments
multiComments: List[CommentRange] = []
# Now all multi line comments
if self.highlightingRules.multiCommentStart and self.highlightingRules.multiCommentStop:
regStart = self.highlightingRules.multiCommentStart
regEnd = self.highlightingRules.multiCommentStop
end = 0
while True:
beginMatch = regStart.search(text, end)
if not beginMatch:
break
beginStart,end = beginMatch.span()
if not self.isInsideComment(beginStart):
while True:
endMatch = regEnd.search(text, end)
if not endMatch:
multiComments.append (CommentRange(beginStart, len(text) - beginStart))
break
endStart,end = endMatch.span()
if not self.isInsideComment(endStart):
multiComments.append (CommentRange(beginStart, end - beginStart))
break
comments.extend(multiComments)
comments.sort()
# Remove comments which are completely included in other comments
i = 1
while True:
if i >= len(comments):
break
prevComment = comments[i-1]
comment = comments[i]
if comment.index >= prevComment.index and comment.index + comment.length < prevComment.index + prevComment.length:
del comments[i]
else:
i += 1
self.comments = comments
def setSearchData (self, searchData: IStringMatcher) -> None:
"""searchData must support the function 'matches' which yields the tuple (start, length) for each match."""
self.searchDatas[0] = searchData
def setSearchData2 (self, searchData: IStringMatcher) -> None:
"""searchData must support the function 'matches' which yields the tuple (start, length) for each match."""
self.searchDatas[1] = searchData
def highlightBlock(self, position: int, text: str) -> List[Tuple[QTextCharFormat, int, int]]:
formats: List[Tuple[QTextCharFormat, int, int]] = []
if not self.highlightingRules:
return formats
if self.highlightingRules.defaultFormat:
formats.append((self.highlightingRules.defaultFormat, 0, len(text)))
# Single line highlighting rules
for expression, fmt in self.highlightingRules.rules:
match = expression.search(text)
while match:
start,end = match.span()
formats.append((fmt, start, end-start))
match = expression.search(text, end)
# Colorize comments
pos = bisect.bisect_right (self.comments, CommentRange(position))
if pos > 0:
pos -= 1
while pos < len(self.comments):
comment = self.comments[pos]
# Comment starts before end of line
if comment.index < position+len(text):
formats.append((self.highlightingRules.commentFormat, comment.index-position, comment.length))
else:
break
pos += 1
# Highlight search match
for index, searchData in enumerate(self.searchDatas):
if searchData:
for matchPos, length in searchData.matches (text):
formats.append((self.searchStringFormats[index], matchPos, length))
return formats
def isInsideComment(self, position: int) -> bool:
if not self.comments:
return False
pos = bisect.bisect_right (self.comments, CommentRange(position))
if pos > 0:
pos -= 1
comment = self.comments[pos]
if comment.index <= position < comment.index + comment.length:
return True
return False
Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.