]>
Commit | Line | Data |
---|---|---|
0c977dbc | 1 | package DiffHighlight; |
927a13fe | 2 | |
8d00662d | 3 | use 5.008; |
2b21008d JK |
4 | use warnings FATAL => 'all'; |
5 | use strict; | |
6 | ||
6804ba3a CW |
7 | # Use the correct value for both UNIX and Windows (/dev/null vs nul) |
8 | use File::Spec; | |
9 | ||
10 | my $NULL = File::Spec->devnull(); | |
11 | ||
927a13fe JK |
12 | # Highlight by reversing foreground and background. You could do |
13 | # other things like bold or underline if you prefer. | |
bca45fbc JK |
14 | my @OLD_HIGHLIGHT = ( |
15 | color_config('color.diff-highlight.oldnormal'), | |
16 | color_config('color.diff-highlight.oldhighlight', "\x1b[7m"), | |
17 | color_config('color.diff-highlight.oldreset', "\x1b[27m") | |
18 | ); | |
19 | my @NEW_HIGHLIGHT = ( | |
20 | color_config('color.diff-highlight.newnormal', $OLD_HIGHLIGHT[0]), | |
21 | color_config('color.diff-highlight.newhighlight', $OLD_HIGHLIGHT[1]), | |
22 | color_config('color.diff-highlight.newreset', $OLD_HIGHLIGHT[2]) | |
23 | ); | |
24 | ||
25 | my $RESET = "\x1b[m"; | |
927a13fe | 26 | my $COLOR = qr/\x1b\[[0-9;]*m/; |
097128d1 | 27 | my $BORING = qr/$COLOR|\s/; |
927a13fe | 28 | |
34d9819e JK |
29 | my @removed; |
30 | my @added; | |
31 | my $in_hunk; | |
4551fbba | 32 | my $graph_indent = 0; |
927a13fe | 33 | |
0c977dbc JK |
34 | our $line_cb = sub { print @_ }; |
35 | our $flush_cb = sub { local $| = 1 }; | |
36 | ||
4551fbba JK |
37 | # Count the visible width of a string, excluding any terminal color sequences. |
38 | sub visible_width { | |
0c977dbc | 39 | local $_ = shift; |
4551fbba JK |
40 | my $ret = 0; |
41 | while (length) { | |
42 | if (s/^$COLOR//) { | |
43 | # skip colors | |
44 | } elsif (s/^.//) { | |
45 | $ret++; | |
46 | } | |
47 | } | |
48 | return $ret; | |
49 | } | |
50 | ||
51 | # Return a substring of $str, omitting $len visible characters from the | |
52 | # beginning, where terminal color sequences do not count as visible. | |
53 | sub visible_substr { | |
54 | my ($str, $len) = @_; | |
55 | while ($len > 0) { | |
56 | if ($str =~ s/^$COLOR//) { | |
57 | next | |
58 | } | |
59 | $str =~ s/^.//; | |
60 | $len--; | |
61 | } | |
62 | return $str; | |
63 | } | |
64 | ||
65 | sub handle_line { | |
66 | my $orig = shift; | |
67 | local $_ = $orig; | |
68 | ||
69 | # match a graph line that begins a commit | |
70 | if (/^(?:$COLOR?\|$COLOR?[ ])* # zero or more leading "|" with space | |
71 | $COLOR?\*$COLOR?[ ] # a "*" with its trailing space | |
72 | (?:$COLOR?\|$COLOR?[ ])* # zero or more trailing "|" | |
73 | [ ]* # trailing whitespace for merges | |
74 | /x) { | |
3b3c79f6 | 75 | my $graph_prefix = $&; |
4551fbba JK |
76 | |
77 | # We must flush before setting graph indent, since the | |
78 | # new commit may be indented differently from what we | |
79 | # queued. | |
80 | flush(); | |
81 | $graph_indent = visible_width($graph_prefix); | |
82 | ||
83 | } elsif ($graph_indent) { | |
84 | if (length($_) < $graph_indent) { | |
85 | $graph_indent = 0; | |
86 | } else { | |
87 | $_ = visible_substr($_, $graph_indent); | |
88 | } | |
89 | } | |
251e7dad | 90 | |
34d9819e | 91 | if (!$in_hunk) { |
4551fbba JK |
92 | $line_cb->($orig); |
93 | $in_hunk = /^$COLOR*\@\@ /; | |
34d9819e | 94 | } |
4551fbba JK |
95 | elsif (/^$COLOR*-/) { |
96 | push @removed, $orig; | |
34d9819e | 97 | } |
4551fbba JK |
98 | elsif (/^$COLOR*\+/) { |
99 | push @added, $orig; | |
927a13fe JK |
100 | } |
101 | else { | |
009a81ed | 102 | flush(); |
4551fbba JK |
103 | $line_cb->($orig); |
104 | $in_hunk = /^$COLOR*[\@ ]/; | |
927a13fe JK |
105 | } |
106 | ||
107 | # Most of the time there is enough output to keep things streaming, | |
108 | # but for something like "git log -Sfoo", you can get one early | |
109 | # commit and then many seconds of nothing. We want to show | |
110 | # that one commit as soon as possible. | |
111 | # | |
112 | # Since we can receive arbitrary input, there's no optimal | |
113 | # place to flush. Flushing on a blank line is a heuristic that | |
114 | # happens to match git-log output. | |
115 | if (!length) { | |
0c977dbc | 116 | $flush_cb->(); |
927a13fe JK |
117 | } |
118 | } | |
119 | ||
0c977dbc JK |
120 | sub flush { |
121 | # Flush any queued hunk (this can happen when there is no trailing | |
122 | # context in the final diff of the input). | |
123 | show_hunk(\@removed, \@added); | |
009a81ed JK |
124 | @removed = (); |
125 | @added = (); | |
0c977dbc | 126 | } |
927a13fe | 127 | |
0c977dbc JK |
128 | sub highlight_stdin { |
129 | while (<STDIN>) { | |
130 | handle_line($_); | |
131 | } | |
132 | flush(); | |
133 | } | |
927a13fe | 134 | |
bca45fbc JK |
135 | # Ideally we would feed the default as a human-readable color to |
136 | # git-config as the fallback value. But diff-highlight does | |
137 | # not otherwise depend on git at all, and there are reports | |
138 | # of it being used in other settings. Let's handle our own | |
139 | # fallback, which means we will work even if git can't be run. | |
140 | sub color_config { | |
141 | my ($key, $default) = @_; | |
6804ba3a | 142 | my $s = `git config --get-color $key 2>$NULL`; |
bca45fbc JK |
143 | return length($s) ? $s : $default; |
144 | } | |
145 | ||
6463fd7e JK |
146 | sub show_hunk { |
147 | my ($a, $b) = @_; | |
148 | ||
34d9819e JK |
149 | # If one side is empty, then there is nothing to compare or highlight. |
150 | if (!@$a || !@$b) { | |
0c977dbc | 151 | $line_cb->(@$a, @$b); |
34d9819e JK |
152 | return; |
153 | } | |
154 | ||
155 | # If we have mismatched numbers of lines on each side, we could try to | |
156 | # be clever and match up similar lines. But for now we are simple and | |
157 | # stupid, and only handle multi-line hunks that remove and add the same | |
158 | # number of lines. | |
159 | if (@$a != @$b) { | |
0c977dbc | 160 | $line_cb->(@$a, @$b); |
34d9819e JK |
161 | return; |
162 | } | |
163 | ||
164 | my @queue; | |
165 | for (my $i = 0; $i < @$a; $i++) { | |
166 | my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]); | |
0c977dbc | 167 | $line_cb->($rm); |
34d9819e JK |
168 | push @queue, $add; |
169 | } | |
0c977dbc | 170 | $line_cb->(@queue); |
6463fd7e JK |
171 | } |
172 | ||
173 | sub highlight_pair { | |
927a13fe JK |
174 | my @a = split_line(shift); |
175 | my @b = split_line(shift); | |
176 | ||
177 | # Find common prefix, taking care to skip any ansi | |
178 | # color codes. | |
179 | my $seen_plusminus; | |
180 | my ($pa, $pb) = (0, 0); | |
181 | while ($pa < @a && $pb < @b) { | |
182 | if ($a[$pa] =~ /$COLOR/) { | |
183 | $pa++; | |
184 | } | |
185 | elsif ($b[$pb] =~ /$COLOR/) { | |
186 | $pb++; | |
187 | } | |
188 | elsif ($a[$pa] eq $b[$pb]) { | |
189 | $pa++; | |
190 | $pb++; | |
191 | } | |
192 | elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') { | |
193 | $seen_plusminus = 1; | |
194 | $pa++; | |
195 | $pb++; | |
196 | } | |
197 | else { | |
198 | last; | |
199 | } | |
200 | } | |
201 | ||
202 | # Find common suffix, ignoring colors. | |
203 | my ($sa, $sb) = ($#a, $#b); | |
204 | while ($sa >= $pa && $sb >= $pb) { | |
205 | if ($a[$sa] =~ /$COLOR/) { | |
206 | $sa--; | |
207 | } | |
208 | elsif ($b[$sb] =~ /$COLOR/) { | |
209 | $sb--; | |
210 | } | |
211 | elsif ($a[$sa] eq $b[$sb]) { | |
212 | $sa--; | |
213 | $sb--; | |
214 | } | |
215 | else { | |
216 | last; | |
217 | } | |
218 | } | |
219 | ||
097128d1 | 220 | if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { |
bca45fbc JK |
221 | return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT), |
222 | highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT); | |
097128d1 JK |
223 | } |
224 | else { | |
6463fd7e JK |
225 | return join('', @a), |
226 | join('', @b); | |
097128d1 | 227 | } |
927a13fe JK |
228 | } |
229 | ||
7e4ffb4c BH |
230 | # we split either by $COLOR or by character. This has the side effect of |
231 | # leaving in graph cruft. It works because the graph cruft does not contain "-" | |
232 | # or "+" | |
927a13fe JK |
233 | sub split_line { |
234 | local $_ = shift; | |
8d00662d KM |
235 | return utf8::decode($_) ? |
236 | map { utf8::encode($_); $_ } | |
237 | map { /$COLOR/ ? $_ : (split //) } | |
238 | split /($COLOR+)/ : | |
239 | map { /$COLOR/ ? $_ : (split //) } | |
240 | split /($COLOR+)/; | |
927a13fe JK |
241 | } |
242 | ||
6463fd7e | 243 | sub highlight_line { |
bca45fbc JK |
244 | my ($line, $prefix, $suffix, $theme) = @_; |
245 | ||
246 | my $start = join('', @{$line}[0..($prefix-1)]); | |
247 | my $mid = join('', @{$line}[$prefix..$suffix]); | |
248 | my $end = join('', @{$line}[($suffix+1)..$#$line]); | |
249 | ||
250 | # If we have a "normal" color specified, then take over the whole line. | |
251 | # Otherwise, we try to just manipulate the highlighted bits. | |
252 | if (defined $theme->[0]) { | |
253 | s/$COLOR//g for ($start, $mid, $end); | |
254 | chomp $end; | |
255 | return join('', | |
256 | $theme->[0], $start, $RESET, | |
257 | $theme->[1], $mid, $RESET, | |
258 | $theme->[0], $end, $RESET, | |
259 | "\n" | |
260 | ); | |
261 | } else { | |
262 | return join('', | |
263 | $start, | |
264 | $theme->[1], $mid, $theme->[2], | |
265 | $end | |
266 | ); | |
267 | } | |
927a13fe | 268 | } |
097128d1 JK |
269 | |
270 | # Pairs are interesting to highlight only if we are going to end up | |
271 | # highlighting a subset (i.e., not the whole line). Otherwise, the highlighting | |
272 | # is just useless noise. We can detect this by finding either a matching prefix | |
273 | # or suffix (disregarding boring bits like whitespace and colorization). | |
274 | sub is_pair_interesting { | |
275 | my ($a, $pa, $sa, $b, $pb, $sb) = @_; | |
276 | my $prefix_a = join('', @$a[0..($pa-1)]); | |
277 | my $prefix_b = join('', @$b[0..($pb-1)]); | |
278 | my $suffix_a = join('', @$a[($sa+1)..$#$a]); | |
279 | my $suffix_b = join('', @$b[($sb+1)..$#$b]); | |
280 | ||
4551fbba JK |
281 | return visible_substr($prefix_a, $graph_indent) !~ /^$COLOR*-$BORING*$/ || |
282 | visible_substr($prefix_b, $graph_indent) !~ /^$COLOR*\+$BORING*$/ || | |
097128d1 JK |
283 | $suffix_a !~ /^$BORING*$/ || |
284 | $suffix_b !~ /^$BORING*$/; | |
285 | } |