Module: WhyClasses::Formatters::DiffFormatter::UnifiedDiff

Defined in:
lib/why_classes/formatters/diff_formatter.rb

Overview

A small LCS-based unified diff over lines. Sufficient for the localized rewrites this tool produces. Ops carry the actual line text.

Defined Under Namespace

Classes: Op

Class Method Summary collapse

Class Method Details

.diff_ops(a, b) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/why_classes/formatters/diff_formatter.rb', line 36

def diff_ops(a, b)
  table = lcs_table(a, b)
  ops = []
  i = a.size
  j = b.size
  while i.positive? && j.positive?
    if a[i - 1] == b[j - 1]
      ops.unshift(Op.new(:eq, a[i - 1], i, j))
      i -= 1
      j -= 1
    elsif table[i - 1][j] >= table[i][j - 1]
      ops.unshift(Op.new(:del, a[i - 1], i, nil))
      i -= 1
    else
      ops.unshift(Op.new(:add, b[j - 1], nil, j))
      j -= 1
    end
  end
  ops.unshift(Op.new(:del, a[(i -= 1)], i + 1, nil)) while i.positive?
  ops.unshift(Op.new(:add, b[(j -= 1)], nil, j + 1)) while j.positive?
  ops
end

.ensure_newline(text) ⇒ Object



101
102
103
# File 'lib/why_classes/formatters/diff_formatter.rb', line 101

def ensure_newline(text)
  text.end_with?("\n") ? text : "#{text}\n"
end

.format_hunk(slice) ⇒ Object



87
88
89
90
91
92
93
94
95
# File 'lib/why_classes/formatters/diff_formatter.rb', line 87

def format_hunk(slice)
  a_lines = slice.select { |op| op.kind != :add }
  b_lines = slice.select { |op| op.kind != :del }
  a_start = a_lines.first&.a_line || 0
  b_start = b_lines.first&.b_line || 0
  header = "@@ -#{a_start},#{a_lines.size} +#{b_start},#{b_lines.size} @@\n"
  body = slice.map { |op| "#{prefix(op.kind)}#{ensure_newline(op.text)}" }.join
  header + body
end

.hunks(ops, context) ⇒ Object

Slice ops into hunks around changes, keeping context equal lines.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/why_classes/formatters/diff_formatter.rb', line 70

def hunks(ops, context)
  change_idx = ops.each_index.select { |k| ops[k].kind != :eq }
  return [] if change_idx.empty?

  ranges = []
  change_idx.each do |k|
    lo = [k - context, 0].max
    hi = [k + context, ops.size - 1].min
    if ranges.any? && lo <= ranges.last[1] + 1
      ranges.last[1] = hi
    else
      ranges << [lo, hi]
    end
  end
  ranges.map { |lo, hi| ops[lo..hi] }
end

.lcs_table(a, b) ⇒ Object



59
60
61
62
63
64
65
66
67
# File 'lib/why_classes/formatters/diff_formatter.rb', line 59

def lcs_table(a, b)
  table = Array.new(a.size + 1) { Array.new(b.size + 1, 0) }
  a.size.times do |i|
    b.size.times do |j|
      table[i + 1][j + 1] = a[i] == b[j] ? table[i][j] + 1 : [table[i][j + 1], table[i + 1][j]].max
    end
  end
  table
end

.prefix(kind) ⇒ Object



97
98
99
# File 'lib/why_classes/formatters/diff_formatter.rb', line 97

def prefix(kind)
  { eq: " ", del: "-", add: "+" }.fetch(kind)
end

.render(before, after, context: 3) ⇒ Object



29
30
31
32
33
34
# File 'lib/why_classes/formatters/diff_formatter.rb', line 29

def render(before, after, context: 3)
  a = before.lines
  b = after.lines
  ops = diff_ops(a, b)
  hunks(ops, context).map { |slice| format_hunk(slice) }.join
end