|
@@ -59,7 +59,6 @@ COLORS = {
|
59
|
59
|
'lightcyan' : '\x1b[1;36m',
|
60
|
60
|
}
|
61
|
61
|
|
62
|
|
-
|
63
|
62
|
# Keys for revision control probe, diff and log with diff
|
64
|
63
|
VCS_INFO = {
|
65
|
64
|
'Git': {
|
|
@@ -84,6 +83,87 @@ def colorize(text, start_color, end_color='reset'):
|
84
|
83
|
return COLORS[start_color] + text + COLORS[end_color]
|
85
|
84
|
|
86
|
85
|
|
|
86
|
+def strsplit(text, width):
|
|
87
|
+ r"""strsplit() splits a given string into two substrings, respecting the
|
|
88
|
+ escape sequences (in a global var COLORS).
|
|
89
|
+
|
|
90
|
+ It returns 3-tuple: (first string, second string, number of visible chars
|
|
91
|
+ in the first string).
|
|
92
|
+
|
|
93
|
+ If some color was active at the splitting point, then the first string is
|
|
94
|
+ appended with the resetting sequence, and the second string is prefixed
|
|
95
|
+ with all active colors.
|
|
96
|
+ """
|
|
97
|
+ first = ''
|
|
98
|
+ second = ''
|
|
99
|
+ found_colors = []
|
|
100
|
+ chars_cnt = 0
|
|
101
|
+ bytes_cnt = 0
|
|
102
|
+ while len(text) > 0:
|
|
103
|
+ # First of all, check if current string begins with any escape
|
|
104
|
+ # sequence.
|
|
105
|
+ append_len = 0
|
|
106
|
+ for color in COLORS:
|
|
107
|
+ if text.startswith(COLORS[color]):
|
|
108
|
+ if color == 'reset':
|
|
109
|
+ found_colors = []
|
|
110
|
+ else:
|
|
111
|
+ found_colors.append(color)
|
|
112
|
+ append_len = len(COLORS[color])
|
|
113
|
+ break
|
|
114
|
+
|
|
115
|
+ if append_len == 0:
|
|
116
|
+ # Current string does not start with any escape sequence, so,
|
|
117
|
+ # either add one more visible char to the "first" string, or
|
|
118
|
+ # break if that string is already large enough.
|
|
119
|
+ if chars_cnt >= width:
|
|
120
|
+ break
|
|
121
|
+ chars_cnt += 1
|
|
122
|
+ append_len = 1
|
|
123
|
+
|
|
124
|
+ first += text[:append_len]
|
|
125
|
+ text = text[append_len:]
|
|
126
|
+ bytes_cnt += append_len
|
|
127
|
+
|
|
128
|
+ second = text
|
|
129
|
+
|
|
130
|
+ # If the first string has some active colors at the splitting point,
|
|
131
|
+ # reset it and append the same colors to the second string
|
|
132
|
+ if len(found_colors) > 0:
|
|
133
|
+ first += COLORS['reset']
|
|
134
|
+ for color in found_colors:
|
|
135
|
+ second = COLORS[color] + second
|
|
136
|
+
|
|
137
|
+ return (first, second, chars_cnt)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+def strtrim(text, width, wrap_char, pad):
|
|
141
|
+ r"""strtrim() trims given string respecting the escape sequences (using
|
|
142
|
+ strsplit), so that if text is larger than width, it's trimmed to have
|
|
143
|
+ width-1 chars plus wrap_char. Additionally, if pad is True, short strings
|
|
144
|
+ are padded with space to have exactly needed width.
|
|
145
|
+
|
|
146
|
+ Returns resulting string.
|
|
147
|
+ """
|
|
148
|
+ text, _, tlen = strsplit(text, width + 1)
|
|
149
|
+ if tlen > width:
|
|
150
|
+ text, _, _ = strsplit(text, width - 1)
|
|
151
|
+
|
|
152
|
+ # Old code always added trailing 'reset' sequence, but strsplit is
|
|
153
|
+ # smarter and only adds it when there is something to reset. However,
|
|
154
|
+ # in order not to distract with changed test data, here's workaround
|
|
155
|
+ # which keeps output exactly the same. TODO: remove it; it doesn't add
|
|
156
|
+ # any practical value for the user.
|
|
157
|
+ if not text.endswith(COLORS['reset']):
|
|
158
|
+ text += COLORS['reset']
|
|
159
|
+
|
|
160
|
+ text += wrap_char
|
|
161
|
+ elif pad:
|
|
162
|
+ # The string is short enough, but it might need to be padded.
|
|
163
|
+ text = "%s%*s" % (text, width - tlen, '')
|
|
164
|
+ return text
|
|
165
|
+
|
|
166
|
+
|
87
|
167
|
class Hunk(object):
|
88
|
168
|
|
89
|
169
|
def __init__(self, hunk_headers, hunk_meta, old_addr, new_addr):
|
|
@@ -398,11 +478,13 @@ class DiffParser(object):
|
398
|
478
|
|
399
|
479
|
class DiffMarker(object):
|
400
|
480
|
|
401
|
|
- def markup(self, diffs, side_by_side=False, width=0, tab_width=8):
|
|
481
|
+ def markup(self, diffs, side_by_side=False, width=0, tab_width=8,
|
|
482
|
+ wrap=False):
|
402
|
483
|
"""Returns a generator"""
|
403
|
484
|
if side_by_side:
|
404
|
485
|
for diff in diffs:
|
405
|
|
- for line in self._markup_side_by_side(diff, width, tab_width):
|
|
486
|
+ for line in self._markup_side_by_side(diff, width, tab_width,
|
|
487
|
+ wrap):
|
406
|
488
|
yield line
|
407
|
489
|
else:
|
408
|
490
|
for diff in diffs:
|
|
@@ -442,33 +524,21 @@ class DiffMarker(object):
|
442
|
524
|
else:
|
443
|
525
|
yield self._markup_common(' ' + old[1])
|
444
|
526
|
|
445
|
|
- def _markup_side_by_side(self, diff, width, tab_width):
|
|
527
|
+ def _markup_side_by_side(self, diff, width, tab_width, wrap):
|
446
|
528
|
"""Returns a generator"""
|
447
|
|
- wrap_char = colorize('>', 'lightmagenta')
|
448
|
529
|
|
449
|
530
|
def _normalize(line):
|
450
|
531
|
return line.replace(
|
451
|
532
|
'\t', ' ' * tab_width).replace('\n', '').replace('\r', '')
|
452
|
533
|
|
453
|
|
- def _fit_with_marker(text, markup_fn, width, pad=False):
|
454
|
|
- """Wrap or pad input pure text, then markup"""
|
455
|
|
- if len(text) > width:
|
456
|
|
- return markup_fn(text[:(width - 1)]) + wrap_char
|
457
|
|
- elif pad:
|
458
|
|
- pad_len = width - len(text)
|
459
|
|
- return '%s%*s' % (markup_fn(text), pad_len, '')
|
460
|
|
- else:
|
461
|
|
- return markup_fn(text)
|
462
|
|
-
|
463
|
|
- def _fit_with_marker_mix(text, base_color, width, pad=False):
|
464
|
|
- """Wrap or pad input text which contains mdiff tags, markup at the
|
465
|
|
- meantime, note only left side need to set `pad`
|
|
534
|
+ def _fit_with_marker_mix(text, base_color, width):
|
|
535
|
+ """Wrap input text which contains mdiff tags, markup at the
|
|
536
|
+ meantime
|
466
|
537
|
"""
|
467
|
538
|
out = [COLORS[base_color]]
|
468
|
|
- count = 0
|
469
|
539
|
tag_re = re.compile(r'\x00[+^-]|\x01')
|
470
|
540
|
|
471
|
|
- while text and count < width:
|
|
541
|
+ while text:
|
472
|
542
|
if text.startswith('\x00-'): # del
|
473
|
543
|
out.append(COLORS['reverse'] + COLORS[base_color])
|
474
|
544
|
text = text[2:]
|
|
@@ -479,6 +549,9 @@ class DiffMarker(object):
|
479
|
549
|
out.append(COLORS['underline'] + COLORS[base_color])
|
480
|
550
|
text = text[2:]
|
481
|
551
|
elif text.startswith('\x01'): # reset
|
|
552
|
+ # TODO: Append resetting sequence if only there is some
|
|
553
|
+ # text after that. That is, call out.append(...) if only
|
|
554
|
+ # len(text) > 1.
|
482
|
555
|
out.append(COLORS['reset'] + COLORS[base_color])
|
483
|
556
|
text = text[1:]
|
484
|
557
|
else:
|
|
@@ -488,17 +561,9 @@ class DiffMarker(object):
|
488
|
561
|
# this tool never put that kind of symbol in their code :-)
|
489
|
562
|
#
|
490
|
563
|
out.append(text[0])
|
491
|
|
- count += 1
|
492
|
564
|
text = text[1:]
|
493
|
565
|
|
494
|
|
- if count == width and tag_re.sub('', text):
|
495
|
|
- # Was stripped: output fulfil and still has normal char in text
|
496
|
|
- out[-1] = COLORS['reset'] + wrap_char
|
497
|
|
- elif count < width and pad:
|
498
|
|
- pad_len = width - count
|
499
|
|
- out.append('%s%*s' % (COLORS['reset'], pad_len, ''))
|
500
|
|
- else:
|
501
|
|
- out.append(COLORS['reset'])
|
|
566
|
+ out.append(COLORS['reset'])
|
502
|
567
|
|
503
|
568
|
return ''.join(out)
|
504
|
569
|
|
|
@@ -558,31 +623,64 @@ class DiffMarker(object):
|
558
|
623
|
|
559
|
624
|
if changed:
|
560
|
625
|
if not old[0]:
|
561
|
|
- left = '%*s' % (width, ' ')
|
|
626
|
+ left = ''
|
562
|
627
|
right = right.rstrip('\x01')
|
563
|
628
|
if right.startswith('\x00+'):
|
564
|
629
|
right = right[2:]
|
565
|
|
- right = _fit_with_marker(
|
566
|
|
- right, self._markup_new, width)
|
|
630
|
+ right = self._markup_new(right)
|
567
|
631
|
elif not new[0]:
|
568
|
632
|
left = left.rstrip('\x01')
|
569
|
633
|
if left.startswith('\x00-'):
|
570
|
634
|
left = left[2:]
|
571
|
|
- left = _fit_with_marker(left, self._markup_old, width)
|
|
635
|
+ left = self._markup_old(left)
|
572
|
636
|
right = ''
|
573
|
637
|
else:
|
574
|
|
- left = _fit_with_marker_mix(left, 'red', width, 1)
|
|
638
|
+ left = _fit_with_marker_mix(left, 'red', width)
|
575
|
639
|
right = _fit_with_marker_mix(right, 'green', width)
|
576
|
640
|
else:
|
577
|
|
- left = _fit_with_marker(
|
578
|
|
- left, self._markup_common, width, 1)
|
579
|
|
- right = _fit_with_marker(right, self._markup_common, width)
|
580
|
|
- yield line_fmt % {
|
581
|
|
- 'left_num': left_num,
|
582
|
|
- 'left': left,
|
583
|
|
- 'right_num': right_num,
|
584
|
|
- 'right': right
|
585
|
|
- }
|
|
641
|
+ left = self._markup_common(left)
|
|
642
|
+ right = self._markup_common(right)
|
|
643
|
+
|
|
644
|
+ if wrap:
|
|
645
|
+ # Need to wrap long lines, so here we'll iterate,
|
|
646
|
+ # shaving off `width` chars from both left and right
|
|
647
|
+ # strings, until both are empty. Also, line number needs to
|
|
648
|
+ # be printed only for the first part.
|
|
649
|
+ lncur = left_num
|
|
650
|
+ rncur = right_num
|
|
651
|
+ while len(left) > 0 or len(right) > 0:
|
|
652
|
+ # Split both left and right lines, preserving escaping
|
|
653
|
+ # sequences correctly.
|
|
654
|
+ lcur, left, llen = strsplit(left, width)
|
|
655
|
+ rcur, right, rlen = strsplit(right, width)
|
|
656
|
+
|
|
657
|
+ # Pad left line with spaces if needed
|
|
658
|
+ if llen < width:
|
|
659
|
+ lcur = "%s%*s" % (lcur, width - llen, '')
|
|
660
|
+
|
|
661
|
+ yield line_fmt % {
|
|
662
|
+ 'left_num': lncur,
|
|
663
|
+ 'left': lcur,
|
|
664
|
+ 'right_num': rncur,
|
|
665
|
+ 'right': rcur
|
|
666
|
+ }
|
|
667
|
+
|
|
668
|
+ # Clean line numbers for further iterations
|
|
669
|
+ lncur = ''
|
|
670
|
+ rncur = ''
|
|
671
|
+ else:
|
|
672
|
+ # Don't need to wrap long lines; instead, a trailing '>'
|
|
673
|
+ # char needs to be appended.
|
|
674
|
+ wrap_char = colorize('>', 'lightmagenta')
|
|
675
|
+ left = strtrim(left, width, wrap_char, len(right) > 0)
|
|
676
|
+ right = strtrim(right, width, wrap_char, False)
|
|
677
|
+
|
|
678
|
+ yield line_fmt % {
|
|
679
|
+ 'left_num': left_num,
|
|
680
|
+ 'left': left,
|
|
681
|
+ 'right_num': right_num,
|
|
682
|
+ 'right': right
|
|
683
|
+ }
|
586
|
684
|
|
587
|
685
|
def _markup_header(self, line):
|
588
|
686
|
return colorize(line, 'cyan')
|
|
@@ -642,7 +740,8 @@ def markup_to_pager(stream, opts):
|
642
|
740
|
diffs = DiffParser(stream).get_diff_generator()
|
643
|
741
|
marker = DiffMarker()
|
644
|
742
|
color_diff = marker.markup(diffs, side_by_side=opts.side_by_side,
|
645
|
|
- width=opts.width, tab_width=opts.tab_width)
|
|
743
|
+ width=opts.width, tab_width=opts.tab_width,
|
|
744
|
+ wrap=opts.wrap)
|
646
|
745
|
|
647
|
746
|
for line in color_diff:
|
648
|
747
|
pager.stdin.write(line.encode('utf-8'))
|
|
@@ -754,6 +853,9 @@ def main():
|
754
|
853
|
parser.add_option(
|
755
|
854
|
'-t', '--tab-width', type='int', default=8, metavar='N',
|
756
|
855
|
help="""convert tab characters to this many spcaes (default: 8)""")
|
|
856
|
+ parser.add_option(
|
|
857
|
+ '', '--wrap', action='store_true',
|
|
858
|
+ help='wrap long lines in side-by-side view')
|
757
|
859
|
|
758
|
860
|
# Hack: use OptionGroup text for extra help message after option list
|
759
|
861
|
option_group = OptionGroup(
|