2 # Copyright (c) 2008 David Aguilar
3 """This module provides SyntaxHighlighter classes.
4 These classes are installed onto specific cola widgets and
5 implement the diff syntax highlighting.
10 from PyQt4
.QtCore
import Qt
11 from PyQt4
.QtCore
import pyqtProperty
12 from PyQt4
.QtCore
import QVariant
13 from PyQt4
.QtGui
import QFont
14 from PyQt4
.QtGui
import QSyntaxHighlighter
15 from PyQt4
.QtGui
import QTextCharFormat
16 from PyQt4
.QtGui
import QColor
18 def TERMINAL(pattern
):
20 Denotes that a pattern is the final pattern that should
21 be matched. If this pattern matches no other formats
22 will be applied, even if they would have matched.
24 return '__TERMINAL__:%s' % pattern
26 # Cache the results of re.compile so that we don't keep
27 # rebuilding the same regexes whenever stylesheets change
31 def _install_default_colors():
36 default_colors
.update({
37 'color_add': color(Qt
.green
, 128),
38 'color_remove': color(Qt
.red
, 128),
39 'color_begin': color(Qt
.darkCyan
),
40 'color_header': color(Qt
.darkYellow
),
41 'color_stat_add': color(QColor(32, 255, 32)),
42 'color_stat_info': color(QColor(32, 32, 255)),
43 'color_stat_remove': color(QColor(255, 32, 32)),
44 'color_emphasis': color(Qt
.black
),
45 'color_info': color(Qt
.blue
),
46 'color_date': color(Qt
.darkCyan
),
48 _install_default_colors()
50 class GenericSyntaxHighligher(QSyntaxHighlighter
):
51 def __init__(self
, doc
, *args
, **kwargs
):
52 QSyntaxHighlighter
.__init
__(self
, doc
)
53 for attr
, val
in default_colors
.items():
54 setattr(self
, attr
, val
)
55 self
.init(doc
, *args
, **kwargs
)
58 def init(self
, *args
, **kwargs
):
65 def generate_rules(self
):
68 def create_rules(self
, *rules
):
70 raise Exception('create_rules requires an even '
71 'number of arguments.')
72 for idx
, rule
in enumerate(rules
):
75 formats
= rules
[idx
+1]
76 terminal
= rule
.startswith(TERMINAL(''))
78 rule
= rule
[len(TERMINAL('')):]
79 if rule
in _RGX_CACHE
:
80 regex
= _RGX_CACHE
[rule
]
82 regex
= re
.compile(rule
)
83 _RGX_CACHE
[rule
] = regex
84 self
._rules
.append((regex
, formats
, terminal
,))
86 def get_formats(self
, line
):
88 for regex
, fmts
, terminal
in self
._rules
:
89 match
= regex
.match(line
)
91 matched
.append([match
, fmts
])
96 def mkformat(self
, fg
=None, bg
=None, bold
=False):
97 format
= QTextCharFormat()
98 if fg
: format
.setForeground(fg
)
99 if bg
: format
.setBackground(bg
)
100 if bold
: format
.setFontWeight(QFont
.Bold
)
103 def highlightBlock(self
, qstr
):
104 ascii
= qstr
.toAscii().data()
106 formats
= self
.get_formats(ascii
)
107 if not formats
: return
108 for match
, fmts
in formats
:
109 start
= match
.start()
111 groups
= match
.groups()
113 # No groups in the regex, assume this is a single rule
114 # that spans the entire line
116 self
.setFormat(0, len(ascii
), fmts
)
119 # Groups exist, rule is a tuple corresponding to group
120 for grpidx
, group
in enumerate(groups
):
121 # allow empty matches
122 if not group
: continue
123 # allow None as a no-op format
126 self
.setFormat(start
, start
+length
,
130 def set_colors(self
, colordict
):
131 for attr
, val
in colordict
.items():
132 setattr(self
, attr
, val
)
134 class DiffSyntaxHighlighter(GenericSyntaxHighligher
):
135 def init(self
, doc
, whitespace
=True):
136 self
.whitespace
= whitespace
137 GenericSyntaxHighligher
.init(self
, doc
)
139 def generate_rules(self
):
140 diff_begin
= self
.mkformat(self
.color_begin
, bold
=True)
141 diff_head
= self
.mkformat(self
.color_header
)
142 diff_add
= self
.mkformat(bg
=self
.color_add
)
143 diff_remove
= self
.mkformat(bg
=self
.color_remove
)
145 diffstat_info
= self
.mkformat(self
.color_stat_info
, bold
=True)
146 diffstat_add
= self
.mkformat(self
.color_stat_add
, bold
=True)
147 diffstat_remove
= self
.mkformat(self
.color_stat_remove
, bold
=True)
150 bad_ws
= self
.mkformat(Qt
.black
, Qt
.red
)
152 # We specify the whitespace rule last so that it is
153 # applied after the diff addition/removal rules.
154 # The rules for the header
155 diff_bgn_rgx
= TERMINAL('^@@|^\+\+\+|^---')
156 diff_hd1_rgx
= TERMINAL('^diff --git')
157 diff_hd2_rgx
= TERMINAL('^index \S+\.\.\S+')
158 diff_hd3_rgx
= TERMINAL('^new file mode')
159 diff_add_rgx
= TERMINAL('^\+')
160 diff_rmv_rgx
= TERMINAL('^-')
161 diff_sts_rgx
= ('(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
162 diff_sum_rgx
= ('(\s+\d+ files changed[^\d]*)'
163 '(:?\d+ insertions[^\d]*)'
164 '(:?\d+ deletions.*)$')
166 self
.create_rules(diff_bgn_rgx
, diff_begin
,
167 diff_hd1_rgx
, diff_head
,
168 diff_hd2_rgx
, diff_head
,
169 diff_hd3_rgx
, diff_head
,
170 diff_add_rgx
, diff_add
,
171 diff_rmv_rgx
, diff_remove
,
172 diff_sts_rgx
, (None, diffstat_info
,
175 diff_sum_rgx
, (diffstat_info
,
179 self
.create_rules('(..*?)(\s+)$', (None, bad_ws
))
181 class LogSyntaxHighlighter(GenericSyntaxHighligher
):
182 def generate_rules(self
):
183 info
= self
.mkformat(self
.color_info
, bold
=True)
184 emphasis
= self
.mkformat(self
.color_emphasis
, bold
=True)
185 date
= self
.mkformat(self
.color_date
, bold
=True)
187 info_rgx
= '^([^:]+:)(.*)$'
188 date_rgx
= TERMINAL('^\w{3}\W+\w{3}\W+\d+\W+[:0-9]+\W+\d{4}$')
190 self
.create_rules(date_rgx
, date
,
191 info_rgx
, (info
, emphasis
))
193 # This is used as a mixin to generate property callbacks
195 private_attr
= '_'+attr
197 if private_attr
in self
.__dict
__:
198 return self
.__dict
__[private_attr
]
201 def setter(self
, value
):
202 self
.__dict
__[private_attr
] = value
204 return (getter
, setter
)
206 def install_theme_properties(cls
):
207 # Diff GUI colors -- this is controllable via the style sheet
208 for name
in default_colors
:
209 setattr(cls
, name
, pyqtProperty('QColor', *accessors(name
)))
211 def set_theme_properties(widget
):
212 for name
, color
in default_colors
.items():
213 widget
.setProperty(name
, QVariant(color
))
216 if __name__
== '__main__':
218 from PyQt4
import QtCore
, QtGui
219 class SyntaxTestDialog(QtGui
.QDialog
):
220 def __init__(self
, parent
):
221 QtGui
.QDialog
.__init
__(self
, parent
)
223 def setupUi(self
, dialog
):
224 dialog
.resize(QtCore
.QSize(QtCore
.QRect(0,0,720,512).size()).expandedTo(dialog
.minimumSizeHint()))
225 self
.vboxlayout
= QtGui
.QVBoxLayout(dialog
)
226 self
.vboxlayout
.setObjectName('vboxlayout')
227 self
.output_text
= QtGui
.QTextEdit(dialog
)
229 font
.setFamily('Monospace')
230 font
.setPointSize(13)
231 self
.output_text
.setFont(font
)
232 self
.output_text
.setAcceptDrops(False)
233 self
.vboxlayout
.addWidget(self
.output_text
)
234 self
.syntax
= DiffSyntaxHighlighter(self
.output_text
.document())
236 app
= QtGui
.QApplication(sys
.argv
)
237 dialog
= SyntaxTestDialog(app
.activeWindow())