models: remove the need for parameter registration
[git-cola.git] / cola / syntax.py
blob3801de4d5edbd57f8e741f500529d1555b993d08
1 #!/usr/bin/python
2 # Copyright (c) 2008 David Aguilar
3 import re
4 from PyQt4.QtCore import Qt
5 from PyQt4.QtCore import pyqtProperty
6 from PyQt4.QtCore import QVariant
7 from PyQt4.QtGui import QFont
8 from PyQt4.QtGui import QSyntaxHighlighter
9 from PyQt4.QtGui import QTextCharFormat
10 from PyQt4.QtGui import QColor
12 def TERMINAL(pattern):
13 """
14 Denotes that a pattern is the final pattern that should
15 be matched. If this pattern matches no other formats
16 will be applied, even if they would have matched.
17 """
18 return '__TERMINAL__:%s' % pattern
20 # Cache the results of re.compile so that we don't keep
21 # rebuilding the same regexes whenever stylesheets change
22 _RGX_CACHE = {}
24 default_colors = {}
25 def _install_default_colors():
26 def color(c, a=255):
27 qc = QColor(c)
28 qc.setAlpha(a)
29 return qc
30 default_colors.update({
31 'color_add': color(Qt.green, 128),
32 'color_remove': color(Qt.red, 128),
33 'color_begin': color(Qt.darkCyan),
34 'color_header': color(Qt.darkYellow),
35 'color_stat_add': color(QColor(32, 255, 32)),
36 'color_stat_info': color(QColor(32, 32, 255)),
37 'color_stat_remove': color(QColor(255, 32, 32)),
38 'color_emphasis': color(Qt.black),
39 'color_info': color(Qt.blue),
40 'color_date': color(Qt.darkCyan),
42 _install_default_colors()
44 class GenericSyntaxHighligher(QSyntaxHighlighter):
45 def __init__(self, doc, *args, **kwargs):
46 QSyntaxHighlighter.__init__(self, doc)
47 for attr, val in default_colors.items():
48 setattr(self, attr, val)
49 self.init(doc, *args, **kwargs)
50 self.reset()
52 def init(self, *args, **kwargs):
53 pass
55 def reset(self):
56 self._rules = []
57 self.generate_rules()
59 def generate_rules(self):
60 pass
62 def create_rules(self, *rules):
63 if len(rules) % 2:
64 raise Exception('create_rules requires an even '
65 'number of arguments.')
66 for idx, rule in enumerate(rules):
67 if idx % 2:
68 continue
69 formats = rules[idx+1]
70 terminal = rule.startswith(TERMINAL(''))
71 if terminal:
72 rule = rule[len(TERMINAL('')):]
73 if rule in _RGX_CACHE:
74 regex = _RGX_CACHE[rule]
75 else:
76 regex = re.compile(rule)
77 _RGX_CACHE[rule] = regex
78 self._rules.append((regex, formats, terminal,))
80 def get_formats(self, line):
81 matched = []
82 for regex, fmts, terminal in self._rules:
83 match = regex.match(line)
84 if match:
85 matched.append([match, fmts])
86 if terminal:
87 return matched
88 return matched
90 def mkformat(self, fg=None, bg=None, bold=False):
91 format = QTextCharFormat()
92 if fg: format.setForeground(fg)
93 if bg: format.setBackground(bg)
94 if bold: format.setFontWeight(QFont.Bold)
95 return format
97 def highlightBlock(self, qstr):
98 ascii = qstr.toAscii().data()
99 if not ascii: return
100 formats = self.get_formats(ascii)
101 if not formats: return
102 for match, fmts in formats:
103 start = match.start()
104 end = match.end()
105 groups = match.groups()
107 # No groups in the regex, assume this is a single rule
108 # that spans the entire line
109 if not groups:
110 self.setFormat(0, len(ascii), fmts)
111 continue
113 # Groups exist, rule is a tuple corresponding to group
114 for grpidx, group in enumerate(groups):
115 # allow empty matches
116 if not group: continue
117 # allow None as a no-op format
118 length = len(group)
119 if fmts[grpidx]:
120 self.setFormat(start, start+length,
121 fmts[grpidx])
122 start += length
124 def set_colors(self, colordict):
125 for attr, val in colordict.items():
126 setattr(self, attr, val)
128 class DiffSyntaxHighlighter(GenericSyntaxHighligher):
129 def init(self, doc, whitespace=True):
130 self.whitespace = whitespace
131 GenericSyntaxHighligher.init(self, doc)
133 def generate_rules(self):
134 diff_begin = self.mkformat(self.color_begin, bold=True)
135 diff_head = self.mkformat(self.color_header)
136 diff_add = self.mkformat(bg=self.color_add)
137 diff_remove = self.mkformat(bg=self.color_remove)
139 diffstat_info = self.mkformat(self.color_stat_info, bold=True)
140 diffstat_add = self.mkformat(self.color_stat_add, bold=True)
141 diffstat_remove = self.mkformat(self.color_stat_remove, bold=True)
143 if self.whitespace:
144 bad_ws = self.mkformat(Qt.black, Qt.red)
146 # We specify the whitespace rule last so that it is
147 # applied after the diff addition/removal rules.
148 # The rules for the header
149 diff_bgn_rgx = TERMINAL('^@@|^\+\+\+|^---')
150 diff_hd1_rgx = TERMINAL('^diff --git')
151 diff_hd2_rgx = TERMINAL('^index \S+\.\.\S+')
152 diff_hd3_rgx = TERMINAL('^new file mode')
153 diff_add_rgx = TERMINAL('^\+')
154 diff_rmv_rgx = TERMINAL('^-')
155 diff_sts_rgx = ('(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
156 diff_sum_rgx = ('(\s+\d+ files changed[^\d]*)'
157 '(:?\d+ insertions[^\d]*)'
158 '(:?\d+ deletions.*)$')
160 self.create_rules(diff_bgn_rgx, diff_begin,
161 diff_hd1_rgx, diff_head,
162 diff_hd2_rgx, diff_head,
163 diff_hd3_rgx, diff_head,
164 diff_add_rgx, diff_add,
165 diff_rmv_rgx, diff_remove,
166 diff_sts_rgx, (None, diffstat_info,
167 None, diffstat_add,
168 diffstat_remove),
169 diff_sum_rgx, (diffstat_info,
170 diffstat_add,
171 diffstat_remove))
172 if self.whitespace:
173 self.create_rules('(..*?)(\s+)$', (None, bad_ws))
175 class LogSyntaxHighlighter(GenericSyntaxHighligher):
176 def generate_rules(self):
177 info = self.mkformat(self.color_info, bold=True)
178 emphasis = self.mkformat(self.color_emphasis, bold=True)
179 date = self.mkformat(self.color_date, bold=True)
181 info_rgx = '^([^:]+:)(.*)$'
182 date_rgx = TERMINAL('^\w{3}\W+\w{3}\W+\d+\W+[:0-9]+\W+\d{4}$')
184 self.create_rules(date_rgx, date,
185 info_rgx, (info, emphasis))
187 # This is used as a mixin to generate property callbacks
188 def accessors(attr):
189 private_attr = '_'+attr
190 def getter(self):
191 if private_attr in self.__dict__:
192 return self.__dict__[private_attr]
193 else:
194 return None
195 def setter(self, value):
196 self.__dict__[private_attr] = value
197 self.reset_syntax()
198 return (getter, setter)
200 def install_theme_properties(cls):
201 # Diff GUI colors -- this is controllable via the style sheet
202 for name in default_colors:
203 setattr(cls, name, pyqtProperty('QColor', *accessors(name)))
205 def set_theme_properties(widget):
206 for name, color in default_colors.items():
207 widget.setProperty(name, QVariant(color))
210 if __name__ == '__main__':
211 import sys
212 from PyQt4 import QtCore, QtGui
213 class SyntaxTestDialog(QtGui.QDialog):
214 def __init__(self, parent):
215 QtGui.QDialog.__init__(self, parent)
216 self.setupUi(self)
217 def setupUi(self, dialog):
218 dialog.resize(QtCore.QSize(QtCore.QRect(0,0,720,512).size()).expandedTo(dialog.minimumSizeHint()))
219 self.vboxlayout = QtGui.QVBoxLayout(dialog)
220 self.vboxlayout.setObjectName('vboxlayout')
221 self.output_text = QtGui.QTextEdit(dialog)
222 font = QtGui.QFont()
223 font.setFamily('Monospace')
224 font.setPointSize(13)
225 self.output_text.setFont(font)
226 self.output_text.setAcceptDrops(False)
227 self.vboxlayout.addWidget(self.output_text)
228 self.syntax = DiffSyntaxHighlighter(self.output_text.document())
230 app = QtGui.QApplication(sys.argv)
231 dialog = SyntaxTestDialog(app.activeWindow())
232 dialog.show()
233 dialog.exec_()