Highlight changed in individual lines in chunks that have the same number of adds/removes
This commit is contained in:
parent
be9d876997
commit
51d2c01bc1
@ -7,6 +7,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from .collect import lines_for_path
|
from .collect import lines_for_path
|
||||||
|
from .diff_speedup import changed_center
|
||||||
|
|
||||||
left_lines = right_lines = None
|
left_lines = right_lines = None
|
||||||
|
|
||||||
@ -26,14 +27,14 @@ def run_diff(file1, file2, context=3):
|
|||||||
|
|
||||||
class Chunk:
|
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):
|
def __init__(self, left_start, right_start, is_context=False):
|
||||||
self.is_context = is_context
|
self.is_context = is_context
|
||||||
self.left_start = left_start
|
self.left_start = left_start
|
||||||
self.right_start = right_start
|
self.right_start = right_start
|
||||||
self.left_count = self.right_count = 0
|
self.left_count = self.right_count = 0
|
||||||
self.is_change = False
|
self.centers = None
|
||||||
|
|
||||||
def add_line(self):
|
def add_line(self):
|
||||||
self.right_count += 1
|
self.right_count += 1
|
||||||
@ -47,7 +48,7 @@ class Chunk:
|
|||||||
|
|
||||||
def finalize(self):
|
def finalize(self):
|
||||||
if not self.is_context and self.left_count == self.right_count:
|
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):
|
def __repr__(self):
|
||||||
return 'Chunk(is_context={}, left_start={}, left_count={}, right_start={}, right_count={})'.format(
|
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}
|
jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs}
|
||||||
for future in concurrent.futures.as_completed(jobs):
|
for future in concurrent.futures.as_completed(jobs):
|
||||||
key = jobs[future]
|
key = jobs[future]
|
||||||
|
left_path, right_path = key, self.jmap[key]
|
||||||
try:
|
try:
|
||||||
ok, returncode, output = future.result()
|
ok, returncode, output = future.result()
|
||||||
except FileNotFoundError as err:
|
except FileNotFoundError as err:
|
||||||
return 'Could not find the {} executable. Is it in your PATH?'.format(err.filename)
|
return 'Could not find the {} executable. Is it in your PATH?'.format(err.filename)
|
||||||
except Exception as e:
|
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:
|
if not ok:
|
||||||
return output + '\nRunning git diff for {} vs. {} failed'.format(key[0], key[1])
|
return output + '\nRunning git diff for {} vs. {} failed'.format(left_path, right_path)
|
||||||
left_lines = lines_for_path(key[0])
|
left_lines = lines_for_path(left_path)
|
||||||
right_lines = lines_for_path(key[1])
|
right_lines = lines_for_path(right_path)
|
||||||
try:
|
try:
|
||||||
patch = parse_patch(output)
|
patch = parse_patch(output)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -109,6 +109,14 @@ added_margin_format = format_func('added_margin')
|
|||||||
filler_format = format_func('filler')
|
filler_format = format_func('filler')
|
||||||
hunk_margin_format = format_func('hunk_margin')
|
hunk_margin_format = format_func('hunk_margin')
|
||||||
hunk_format = format_func('hunk')
|
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):
|
def title_lines(left_path, right_path, args, columns, margin_size):
|
||||||
@ -137,6 +145,36 @@ def split_to_size(line, width):
|
|||||||
line = line[p:]
|
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}
|
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}
|
text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_format, 'context': text_format}
|
||||||
|
|
||||||
@ -174,7 +212,11 @@ def hunk_title(hunk_num, hunk, margin_size, available_cols):
|
|||||||
return m + hunk_format(place_in(t, available_cols))
|
return m + hunk_format(place_in(t, available_cols))
|
||||||
|
|
||||||
|
|
||||||
def render_half_line(line_number, src, ltype, margin_size, 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)
|
lines = split_to_size(src[line_number], available_cols)
|
||||||
line_number = str(line_number + 1)
|
line_number = str(line_number + 1)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@ -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))
|
ref = Reference(data.left_path, HunkRef(hunk_num, chunk_num, i))
|
||||||
ll, rl = [], []
|
ll, rl = [], []
|
||||||
if i < chunk.left_count:
|
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:
|
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:
|
if i < common:
|
||||||
extra = len(ll) - len(rl)
|
extra = len(ll) - len(rl)
|
||||||
if extra != 0:
|
if extra != 0:
|
||||||
|
|||||||
@ -21,3 +21,14 @@ class TestDiff(BaseTest):
|
|||||||
pc, sc = changed_center(left, right)
|
pc, sc = changed_center(left, right)
|
||||||
for src in (left, right):
|
for src in (left, right):
|
||||||
self.assertEqual((prefix, suffix), (src[:pc], src[-sc:] if sc else ''))
|
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')))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user