cola: add more documentation strings to the cola modules
[git-cola.git] / cola / syntax.py
blob93af48655c88e40fa0adb6fb0fb5c884896339fd
1 #!/usr/bin/python
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.
7 """
9 import re
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):
19 """
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.
23 """
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
28 _RGX_CACHE = {}
30 default_colors = {}
31 def _install_default_colors():
32 def color(c, a=255):
33 qc = QColor(c)
34 qc.setAlpha(a)
35 return qc
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)
56 self.reset()
58 def init(self, *args, **kwargs):
59 pass
61 def reset(self):
62 self._rules = []
63 self.generate_rules()
65 def generate_rules(self):
66 pass
68 def create_rules(self, *rules):
69 if len(rules) % 2:
70 raise Exception('create_rules requires an even '
71 'number of arguments.')
72 for idx, rule in enumerate(rules):
73 if idx % 2:
74 continue
75 formats = rules[idx+1]
76 terminal = rule.startswith(TERMINAL(''))
77 if terminal:
78 rule = rule[len(TERMINAL('')):]
79 if rule in _RGX_CACHE:
80 regex = _RGX_CACHE[rule]
81 else:
82 regex = re.compile(rule)
83 _RGX_CACHE[rule] = regex
84 self._rules.append((regex, formats, terminal,))
86 def get_formats(self, line):
87 matched = []
88 for regex, fmts, terminal in self._rules:
89 match = regex.match(line)
90 if match:
91 matched.append([match, fmts])
92 if terminal:
93 return matched
94 return matched
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)
101 return format
103 def highlightBlock(self, qstr):
104 ascii = qstr.toAscii().data()
105 if not ascii: return
106 formats = self.get_formats(ascii)
107 if not formats: return
108 for match, fmts in formats:
109 start = match.start()
110 end = match.end()
111 groups = match.groups()
113 # No groups in the regex, assume this is a single rule
114 # that spans the entire line
115 if not groups:
116 self.setFormat(0, len(ascii), fmts)
117 continue
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
124 length = len(group)
125 if fmts[grpidx]:
126 self.setFormat(start, start+length,
127 fmts[grpidx])
128 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)
149 if self.whitespace:
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,
173 None, diffstat_add,
174 diffstat_remove),
175 diff_sum_rgx, (diffstat_info,
176 diffstat_add,
177 diffstat_remove))
178 if self.whitespace:
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
194 def accessors(attr):
195 private_attr = '_'+attr
196 def getter(self):
197 if private_attr in self.__dict__:
198 return self.__dict__[private_attr]
199 else:
200 return None
201 def setter(self, value):
202 self.__dict__[private_attr] = value
203 self.reset_syntax()
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__':
217 import sys
218 from PyQt4 import QtCore, QtGui
219 class SyntaxTestDialog(QtGui.QDialog):
220 def __init__(self, parent):
221 QtGui.QDialog.__init__(self, parent)
222 self.setupUi(self)
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)
228 font = QtGui.QFont()
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())
238 dialog.show()
239 dialog.exec_()