cola: include GitPython to make installation simpler for users
[git-cola.git] / cola / syntax.py
blob78bba7a08dd434acb9807a02562fa2c769c04285
1 #!/usr/bin/python
2 import re
3 from PyQt4.QtCore import Qt
4 from PyQt4.QtGui import QFont
5 from PyQt4.QtGui import QSyntaxHighlighter
6 from PyQt4.QtGui import QTextCharFormat
7 from PyQt4.QtGui import QColor
9 class GenericSyntaxHighligher(QSyntaxHighlighter):
11 def __init__(self, doc):
12 QSyntaxHighlighter.__init__(self, doc)
13 self.__rules = []
15 FINAL_STR = '__FINAL__:'
16 def final(self, pattern=''):
17 """
18 Denotes that a pattern is the final pattern that should
19 be matched. If this pattern matches no other formats
20 will be applied, even if they would have matched.
21 """
22 return GenericSyntaxHighligher.FINAL_STR + pattern
24 def create_rules(self, *rules):
25 if len(rules) % 2:
26 raise Exception('create_rules requires an even '
27 'number of arguments.')
28 for idx, rule in enumerate(rules):
29 if idx % 2:
30 continue
31 formats = rules[idx+1]
32 terminal = rule.startswith(self.final())
33 if terminal:
34 rule = rule[len(self.final()):]
35 regex = re.compile(rule)
36 self.__rules.append((regex, formats, terminal,))
38 def get_formats(self, line):
39 matched = []
40 for regex, fmts, terminal in self.__rules:
41 match = regex.match(line)
42 if match:
43 matched.append([match, fmts])
44 if terminal: return matched
45 return matched
47 def mkformat(self, fg=None, bg=None, bold=False):
48 format = QTextCharFormat()
49 if fg: format.setForeground(fg)
50 if bg: format.setBackground(bg)
51 if bold: format.setFontWeight(QFont.Bold)
52 return format
54 def highlightBlock(self, qstr):
55 ascii = qstr.toAscii().data()
56 if not ascii: return
57 formats = self.get_formats(ascii)
58 if not formats: return
59 for match, fmts in formats:
60 start = match.start()
61 end = match.end()
62 groups = match.groups()
64 # No groups in the regex, assume this is a single rule
65 # that spans the entire line
66 if not groups:
67 self.setFormat(0, len(ascii), fmts)
68 continue
70 # Groups exist, rule is a tuple corresponding to group
71 for grpidx, group in enumerate(groups):
72 # allow empty matches
73 if not group: continue
74 # allow None as a no-op format
75 length = len(group)
76 if fmts[grpidx]:
77 self.setFormat(start, start+length,
78 fmts[grpidx])
79 start += length
81 class DiffSyntaxHighlighter(GenericSyntaxHighligher):
82 def __init__(self, doc,whitespace=True):
83 GenericSyntaxHighligher.__init__(self,doc)
85 diffstat = self.mkformat(Qt.blue, bold=True)
86 diffstat_add = self.mkformat(Qt.darkGreen, bold=True)
87 diffstat_remove = self.mkformat(Qt.red, bold=True)
89 bg_green = QColor(Qt.green)
90 bg_green.setAlpha(128)
92 bg_red = QColor(Qt.red)
93 bg_red.setAlpha(128)
95 diff_begin = self.mkformat(Qt.darkCyan, bold=True)
96 diff_head = self.mkformat(Qt.darkYellow)
97 diff_add = self.mkformat(bg=bg_green)
98 diff_remove = self.mkformat(bg=bg_red)
100 if whitespace:
101 bad_ws = self.mkformat(Qt.black, Qt.red)
103 # We specify the whitespace rule last so that it is
104 # applied after the diff addition/removal rules.
105 # The rules for the header
106 diff_bgn_rgx = self.final('^@@|^\+\+\+|^---')
107 diff_hd1_rgx = self.final('^diff --git')
108 diff_hd2_rgx = self.final('^index \S+\.\.\S+')
109 diff_hd3_rgx = self.final('^new file mode')
110 diff_add_rgx = self.final('^\+')
111 diff_rmv_rgx = self.final('^-')
112 diff_sts_rgx = ('(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
113 diff_sum_rgx = ('(\s+\d+ files changed[^\d]*)'
114 '(:?\d+ insertions[^\d]*)'
115 '(:?\d+ deletions.*)$')
117 self.create_rules(diff_bgn_rgx, diff_begin,
118 diff_hd1_rgx, diff_head,
119 diff_hd2_rgx, diff_head,
120 diff_hd3_rgx, diff_head,
121 diff_add_rgx, diff_add,
122 diff_rmv_rgx, diff_remove,
123 diff_sts_rgx, (None, diffstat,
124 None, diffstat_add,
125 diffstat_remove),
126 diff_sum_rgx, (diffstat,
127 diffstat_add,
128 diffstat_remove))
129 if whitespace:
130 self.create_rules('(..*?)(\s+)$', (None, bad_ws))
132 class LogSyntaxHighlighter(GenericSyntaxHighligher):
133 def __init__(self, doc):
134 GenericSyntaxHighligher.__init__(self,doc)
136 blue = self.mkformat(Qt.blue, bold=True)
137 black = self.mkformat(Qt.black, bold=True)
138 dark_cyan = self.mkformat(Qt.darkCyan, bold=True)
140 blue_black_rgx = '^([^:]+:)(.*)$'
141 dark_cyan_rgx = self.final('^\w{3}\W+\w{3}\W+\d+\W+'
142 '[:0-9]+\W+\d{4}$')
144 self.create_rules(dark_cyan_rgx, dark_cyan,
145 blue_black_rgx, (blue, black))
147 if __name__ == '__main__':
148 import sys
149 from PyQt4 import QtCore, QtGui
150 class SyntaxTestDialog(QtGui.QDialog):
151 def __init__(self, parent):
152 QtGui.QDialog.__init__(self, parent)
153 self.setupUi(self)
154 def setupUi(self, dialog):
155 dialog.resize(QtCore.QSize(QtCore.QRect(0,0,720,512).size()).expandedTo(dialog.minimumSizeHint()))
156 self.vboxlayout = QtGui.QVBoxLayout(dialog)
157 self.vboxlayout.setObjectName('vboxlayout')
158 self.output_text = QtGui.QTextEdit(dialog)
159 font = QtGui.QFont()
160 font.setFamily('Monospace')
161 font.setPointSize(13)
162 self.output_text.setFont(font)
163 self.output_text.setAcceptDrops(False)
164 self.vboxlayout.addWidget(self.output_text)
165 DiffSyntaxHighlighter(self.output_text.document())
166 app = QtGui.QApplication(sys.argv)
167 dialog = SyntaxTestDialog(app.activeWindow())
168 dialog.show()
169 dialog.exec_()