More work on the diff kitten

This commit is contained in:
Kovid Goyal 2018-05-02 15:26:47 +05:30
parent 0721951c79
commit bec0f02b11
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 140 additions and 51 deletions

View File

@ -20,12 +20,29 @@ def run_diff(file1, file2, context=3):
return False, returncode, stderr.decode('utf-8')
def even_up_sides(left, right, filler):
delta = len(left) - len(right)
if delta != 0:
dest = left if delta < 0 else right
for i in range(abs(delta)):
dest.append(filler)
class Chunk:
__slots__ = ('is_context', 'left_start', 'right_start', 'left_count', 'right_count')
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
def add_line(self):
self.right_count += 1
def remove_line(self):
self.left_count += 1
def context_line(self):
self.left_count += 1
self.right_count += 1
def __repr__(self):
return 'Chunk(is_context={}, left_start={}, left_count={}, right_start={}, right_count={})'.format(
self.is_context, self.left_start, self.left_count, self.right_start, self.right_count)
class Hunk:
@ -33,34 +50,58 @@ class Hunk:
def __init__(self, title, left, right):
self.left_start, self.left_count = left
self.right_start, self.right_count = right
self.left_start -= 1 # 0-index
self.right_start -= 1 # 0-index
self.title = title
self.left_lines = []
self.right_lines = []
self.left_pos = self.right_pos = 0
self.chunks = []
self.current_chunk = None
self.largest_line_number = max(self.left_start + self.left_count, self.right_start + self.right_count)
def new_chunk(self, is_context=False):
if self.chunks:
c = self.chunks[-1]
left_start = c.left_start + c.left_count
right_start = c.right_start + c.right_count
else:
left_start = self.left_start
right_start = self.right_start
return Chunk(left_start, right_start, is_context)
def ensure_diff_chunk(self):
if self.current_chunk is None:
self.current_chunk = self.new_chunk(is_context=False)
elif self.current_chunk.is_context:
self.chunks.append(self.current_chunk)
self.current_chunk = self.new_chunk(is_context=False)
def ensure_context_chunk(self):
if self.current_chunk is None:
self.current_chunk = self.new_chunk(is_context=True)
elif not self.current_chunk.is_context:
self.chunks.append(self.current_chunk)
self.current_chunk = self.new_chunk(is_context=True)
def add_line(self):
self.right_lines.append((self.right_pos, True))
self.right_pos += 1
self.ensure_diff_chunk()
self.current_chunk.add_line()
def remove_line(self):
self.left_lines.append((self.left_pos, True))
self.left_pos += 1
self.ensure_diff_chunk()
self.current_chunk.remove_line()
def context_line(self):
even_up_sides(self.left_lines, self.right_lines, (None, True))
self.left_lines.append((self.left_pos, False))
self.right_lines.append((self.right_pos, False))
self.left_pos += 1
self.right_pos += 1
self.ensure_context_chunk()
self.current_chunk.context_line()
def finalize(self):
even_up_sides(self.left_lines, self.right_lines, (None, True))
self.chunks.append(self.current_chunk)
del self.current_chunk
# Sanity check
if self.left_pos != self.left_count:
raise ValueError('Left side line mismatch {} != {}'.format(self.left_pos, self.left_count))
if self.right_pos != self.right_count:
raise ValueError('Right side line mismatch {} != {}'.format(self.right_pos, self.right_count))
c = self.chunks[-1]
if c.left_start + c.left_count != self.left_start + self.left_count:
raise ValueError('Left side line mismatch {} != {}'.format(c.left_start + c.left_count, self.left_start + self.left_count))
if c.right_start + c.right_count != self.right_start + self.right_count:
raise ValueError('Left side line mismatch {} != {}'.format(c.right_start + c.right_count, self.right_start + self.right_count))
def parse_range(x):

View File

@ -4,20 +4,21 @@
import re
from gettext import gettext as _
from itertools import repeat
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from .collect import data_for_path, lines_for_path, path_name_map
from .config import formats
from .git import even_up_sides
class HunkRef:
__slots__ = ('hunk_num', 'line_num')
__slots__ = ('hunk_num', 'line_num', 'chunk_num')
def __init__(self, hunk_num, line_num=None):
def __init__(self, hunk_num, chunk_num=None, line_num=None):
self.hunk_num = hunk_num
self.chunk_num = chunk_num
self.line_num = line_num
@ -128,6 +129,8 @@ def binary_lines(path, other_path, columns, margin_size):
def split_to_size(line, width):
if not line:
yield line
while line:
p = truncate_point_for_length(line, width)
yield line[:p]
@ -138,8 +141,20 @@ margin_bg_map = {'filler': filler_format, 'remove': removed_margin_format, 'add'
text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_format, 'context': text_format}
class DiffData:
def __init__(self, left_path, right_path, available_cols, margin_size):
self.left_path, self.right_path = left_path, right_path
self.available_cols = available_cols
self.margin_size = margin_size
self.left_lines, self.right_lines = map(lines_for_path, (left_path, right_path))
self.filler_line = render_diff_line('', '', 'filler', margin_size, available_cols)
self.left_filler_line = render_diff_line('', '', 'remove', margin_size, available_cols)
self.right_filler_line = render_diff_line('', '', 'add', margin_size, available_cols)
def render_diff_line(number, text, ltype, margin_size, available_cols):
margin = margin_bg_map[ltype](place_in(str(number or ''), margin_size))
margin = margin_bg_map[ltype](place_in(number, margin_size))
content = text_bg_map[ltype](fill_in(text or '', available_cols))
return margin + content
@ -155,38 +170,71 @@ def render_diff_pair(left_line_number, left, left_is_change, right_line_number,
def hunk_title(hunk_num, hunk, margin_size, available_cols):
m = hunk_margin_format(' ' * margin_size)
t = '@@ -{},{} +{},{} @@ {}'.format(hunk.left_start, hunk.left_count, hunk.right_start, hunk.right_count, hunk.title)
t = '@@ -{},{} +{},{} @@ {}'.format(hunk.left_start + 1, hunk.left_count, hunk.right_start + 1, hunk.right_count, hunk.title)
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)
line_number = str(line_number)
for line in lines:
yield render_diff_line(line_number, line, ltype, margin_size, available_cols)
line_number = ''
def lines_for_chunk(data, hunk_num, chunk, chunk_num):
if chunk.is_context:
for i in range(chunk.left_count):
left_line_number = chunk.left_start + i
right_line_number = chunk.right_start + i
lines = split_to_size(data.left_lines[left_line_number], data.available_cols)
ref = Reference(data.left_path, HunkRef(hunk_num, chunk_num, i))
left_line_number = str(left_line_number)
right_line_number = str(right_line_number)
for text in lines:
line = render_diff_line(left_line_number, text, 'context', data.margin_size, data.available_cols)
if right_line_number == left_line_number:
r = line
else:
r = render_diff_line(right_line_number, text, 'context', data.margin_size, data.available_cols)
yield Line(line + r, ref)
left_line_number = right_line_number = ''
else:
common = min(chunk.left_count, chunk.right_count)
for i in range(max(chunk.left_count, chunk.right_count)):
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))
if i < chunk.right_count:
rl.extend(render_half_line(chunk.right_start + i, data.right_lines, 'add', data.margin_size, data.available_cols))
if i < common:
extra = len(ll) - len(rl)
if extra != 0:
if extra < 0:
x, fl = ll, data.left_filler_line
extra = -extra
else:
x, fl = rl, data.right_filler_line
x.extend(repeat(fl, extra))
else:
if ll:
x, count = rl, len(ll)
else:
x, count = ll, len(rl)
x.extend(repeat(data.filler_line, count))
for left_line, right_line in zip(ll, rl):
yield Line(left_line + right_line, ref)
def lines_for_diff(left_path, right_path, hunks, args, columns, margin_size):
available_cols = columns // 2 - margin_size
left_lines, right_lines = map(lines_for_path, (left_path, right_path))
data = DiffData(left_path, right_path, available_cols, margin_size)
for hunk_num, hunk in enumerate(hunks):
yield Line(hunk_title(hunk_num, hunk, margin_size, columns - margin_size), Reference(left_path, HunkRef(hunk_num)))
for line_num, (left, right) in enumerate(zip(hunk.left_lines, hunk.right_lines)):
left_line_number, left_is_change = left
right_line_number, right_is_change = right
if left_line_number is None:
left_wrapped_lines = []
else:
left_wrapped_lines = list(split_to_size(left_lines[left_line_number], available_cols))
if right_line_number is None:
right_wrapped_lines = []
else:
right_wrapped_lines = list(split_to_size(right_lines[right_line_number], available_cols))
even_up_sides(left_wrapped_lines, right_wrapped_lines, '')
for i, (left, right) in enumerate(zip(left_wrapped_lines, right_wrapped_lines)):
ln = None if left_line_number is None else hunk.left_start + left_line_number
rn = None if right_line_number is None else hunk.right_start + right_line_number
yield Line(
render_diff_pair(
ln, left, left_is_change, rn, right,
right_is_change, i == 0, margin_size, available_cols
),
Reference(left_path, HunkRef(hunk_num, line_num))
)
for cnum, chunk in enumerate(hunk.chunks):
yield from lines_for_chunk(data, hunk_num, chunk, cnum)
def render_diff(collection, diff_map, args, columns):