diff --git a/kittens/diff/git.py b/kittens/diff/git.py index 73e5477ab..deed213a2 100644 --- a/kittens/diff/git.py +++ b/kittens/diff/git.py @@ -7,6 +7,7 @@ import os import subprocess from .collect import lines_for_path +from .diff_speedup import changed_center left_lines = right_lines = None @@ -26,14 +27,14 @@ def run_diff(file1, file2, context=3): class Chunk: - __slots__ = ('is_context', 'left_start', 'right_start', 'left_count', 'right_count', 'is_change') + __slots__ = ('is_context', 'left_start', 'right_start', 'left_count', 'right_count', 'centers') def __init__(self, left_start, right_start, is_context=False): self.is_context = is_context self.left_start = left_start self.right_start = right_start self.left_count = self.right_count = 0 - self.is_change = False + self.centers = None def add_line(self): self.right_count += 1 @@ -47,7 +48,7 @@ class Chunk: def finalize(self): if not self.is_context and self.left_count == self.right_count: - self.is_change = True + self.centers = tuple(changed_center(left_lines[self.left_start + i], right_lines[self.right_start + i]) for i in range(self.left_count)) def __repr__(self): return 'Chunk(is_context={}, left_start={}, left_count={}, right_start={}, right_count={})'.format( @@ -183,16 +184,17 @@ class Differ: jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs} for future in concurrent.futures.as_completed(jobs): key = jobs[future] + left_path, right_path = key, self.jmap[key] try: ok, returncode, output = future.result() except FileNotFoundError as err: return 'Could not find the {} executable. Is it in your PATH?'.format(err.filename) except Exception as e: - return 'Running git diff for {} vs. {} generated an exception: {}'.format(key[0], key[1], e) + return 'Running git diff for {} vs. {} generated an exception: {}'.format(left_path, right_path, e) if not ok: - return output + '\nRunning git diff for {} vs. {} failed'.format(key[0], key[1]) - left_lines = lines_for_path(key[0]) - right_lines = lines_for_path(key[1]) + return output + '\nRunning git diff for {} vs. {} failed'.format(left_path, right_path) + left_lines = lines_for_path(left_path) + right_lines = lines_for_path(right_path) try: patch = parse_patch(output) except Exception: diff --git a/kittens/diff/render.py b/kittens/diff/render.py index 92f675562..d1d6116b9 100644 --- a/kittens/diff/render.py +++ b/kittens/diff/render.py @@ -109,6 +109,14 @@ added_margin_format = format_func('added_margin') filler_format = format_func('filler') hunk_margin_format = format_func('hunk_margin') hunk_format = format_func('hunk') +highlight_map = {'remove': ('removed_highlight', 'removed'), 'add': ('added_highlight', 'added')} + + +def highlight_boundaries(ltype): + s, e = highlight_map[ltype] + start = '\x1b[' + formats[s] + 'm' + stop = '\x1b[' + formats[e] + 'm' + return start, stop def title_lines(left_path, right_path, args, columns, margin_size): @@ -137,6 +145,36 @@ def split_to_size(line, width): line = line[p:] +def split_to_size_with_center(line, width, prefix_count, suffix_count, start, stop): + pos = state = 0 + suffix_pos = len(line) - suffix_count + while line: + p = truncate_point_for_length(line, width) + if state is 0: + if pos + p > prefix_count: + state = 1 + a, line = line[:p], line[p:] + if pos + p > suffix_pos: + a = a[:suffix_pos - pos] + stop + a[suffix_pos - pos:] + state = 2 + yield a[:prefix_count - pos] + start + a[prefix_count - pos:] + else: + yield line[:p] + line = line[p:] + elif state is 1: + if pos + p > suffix_pos: + state = 2 + a, line = line[:p], line[p:] + yield start + a[:suffix_pos - pos] + stop + a[suffix_pos - pos:] + else: + yield start + line[:p] + line = line[p:] + elif state is 2: + yield line[:p] + line = line[p:] + pos += p + + margin_bg_map = {'filler': filler_format, 'remove': removed_margin_format, 'add': added_margin_format, 'context': margin_format} text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_format, 'context': text_format} @@ -174,8 +212,12 @@ def hunk_title(hunk_num, hunk, margin_size, available_cols): return m + hunk_format(place_in(t, available_cols)) -def render_half_line(line_number, src, ltype, margin_size, available_cols): - lines = split_to_size(src[line_number], available_cols) +def render_half_line(line_number, src, ltype, margin_size, available_cols, changed_center): + if changed_center is not None and changed_center[0]: + start, stop = highlight_boundaries(ltype) + lines = split_to_size_with_center(src[line_number], available_cols, changed_center[0], changed_center[1], start, stop) + else: + lines = split_to_size(src[line_number], available_cols) line_number = str(line_number + 1) for line in lines: yield render_diff_line(line_number, line, ltype, margin_size, available_cols) @@ -205,9 +247,13 @@ def lines_for_chunk(data, hunk_num, chunk, chunk_num): ref = Reference(data.left_path, HunkRef(hunk_num, chunk_num, i)) ll, rl = [], [] if i < chunk.left_count: - ll.extend(render_half_line(chunk.left_start + i, data.left_lines, 'remove', data.margin_size, data.available_cols)) + ll.extend(render_half_line( + chunk.left_start + i, data.left_lines, 'remove', data.margin_size, + data.available_cols, None if chunk.centers is None else chunk.centers[i])) if i < chunk.right_count: - rl.extend(render_half_line(chunk.right_start + i, data.right_lines, 'add', data.margin_size, data.available_cols)) + rl.extend(render_half_line( + chunk.right_start + i, data.right_lines, 'add', data.margin_size, + data.available_cols, None if chunk.centers is None else chunk.centers[i])) if i < common: extra = len(ll) - len(rl) if extra != 0: diff --git a/kitty_tests/diff.py b/kitty_tests/diff.py index 48cd313cc..b32398bc9 100644 --- a/kitty_tests/diff.py +++ b/kitty_tests/diff.py @@ -21,3 +21,14 @@ class TestDiff(BaseTest): pc, sc = changed_center(left, right) for src in (left, right): self.assertEqual((prefix, suffix), (src[:pc], src[-sc:] if sc else '')) + + def test_split_to_size(self): + from kittens.diff.render import split_to_size_with_center + for line, width, prefix_count, suffix_count, expected in [ + ('abcdefgh', 20, 2, 3, ('abSScdeEEfgh',)), + ('abcdefgh', 20, 2, 0, ('abSScdefgh',)), + ('abcdefgh', 3, 2, 3, ('abSSc', 'SSdeEEf', 'gh')), + ('abcdefgh', 2, 4, 1, ('ab', 'cd', 'SSef', 'SSgEEh')), + ]: + self.ae(expected, tuple(split_to_size_with_center( + line, width, prefix_count, suffix_count, 'SS', 'EE')))