Class: Refiner

Inherits:
Object
  • Object
show all
Includes:
Colors
Defined in:
lib/refiner.rb

Overview

Compute longest common substring based diff between two strings.

The diff format is first the old string:

  • in red

  • with each line prefixed with minuses

  • removed characters highlighted in inverse video

Then comes the new string:

  • in green

  • with each line prefixed with plusses

  • added characters highlighted in inverse video

Constant Summary collapse

REFINEMENT_THRESHOLD =

If either old or new would get more than this percentage of chars highlighted, consider this to be a replacement rather than a change and just don’t highlight anything.

30

Constants included from Colors

Colors::BOLD, Colors::CYAN, Colors::DEFAULT_COLOR, Colors::ESC, Colors::GREEN, Colors::NOT_REVERSE, Colors::RED, Colors::RESET, Colors::REVERSE

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Colors

#reversed, #uncolor

Constructor Details

#initialize(old, new) ⇒ Refiner

Returns a new instance of Refiner.



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/refiner.rb', line 152

def initialize(old, new)
  old_highlights, new_highlights = try_highlight(old, new)
  if old_highlights.size == 0 && new_highlights.size == 0
    old_highlights, new_highlights = try_highlight_initial_lines(old, new)
  end

  if !create_one_to_many_refinements(old, new,
                                     old_highlights,
                                     new_highlights)
    create_refinements(old, new, old_highlights, new_highlights)
  end
end

Instance Attribute Details

#refined_newObject (readonly)

Returns the value of attribute refined_new.



20
21
22
# File 'lib/refiner.rb', line 20

def refined_new
  @refined_new
end

#refined_oldObject (readonly)

Returns the value of attribute refined_old.



19
20
21
# File 'lib/refiner.rb', line 19

def refined_old
  @refined_old
end

Instance Method Details

#censor_highlights(old, new, old_highlights, new_highlights) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/refiner.rb', line 42

def censor_highlights(old, new, old_highlights, new_highlights)
  old_highlights_percentage = 100 * old_highlights.size / old.length
  new_highlights_percentage = 100 * new_highlights.size / new.length

  if old_highlights_percentage > REFINEMENT_THRESHOLD \
     || new_highlights_percentage > REFINEMENT_THRESHOLD
    # We'll consider this a replacement rather than a change, don't
    # highlight it.
    old_highlights.clear
    new_highlights.clear
  end
end

#collect_highlights(diff, old_highlights, new_highlights) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/refiner.rb', line 27

def collect_highlights(diff, old_highlights, new_highlights)
  diff.each do |section|
    section.each do |highlight|
      case highlight.action
      when '-'
        old_highlights << highlight.position
      when '+'
        new_highlights << highlight.position
      else
        fail("Unsupported diff type: <#{type}>")
      end
    end
  end
end

#create_one_to_many_refinements(old, new, old_highlights, new_highlights) ⇒ Object

After returning from this method, both @refined_old and @refined_new must have been set to reasonable values.

Returns false if the preconditions for using this method aren’t fulfilled



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/refiner.rb', line 119

def create_one_to_many_refinements(old, new, old_highlights, new_highlights)
  # If things have been removed from the first line, the specialized
  # highlighting won't work
  return false if old_highlights.count > 0

  # If the first line was replaced rather than updated, the specialized
  # highlighting won't work
  return false if new_highlights.count == 0

  # Specialized highlighting requires exactly one old line
  return false if old.lines.count != 1

  lines = new.lines
  # Specialized highlighting requires two or more new lines
  return false if lines.count < 2

  @refined_old = ''

  refined_line_1 =
    render_refinement(' ', '', lines[0], new_highlights,
                      highlight_color: GREEN)

  line_2_index_0 = lines[0].length
  refined_remaining_lines = render_refinement('+', GREEN,
                                              lines[1..-1].join,
                                              new_highlights,
                                              base_index: line_2_index_0)

  @refined_new = refined_line_1 + refined_remaining_lines

  return true
end

#create_refinements(old, new, old_highlights, new_highlights) ⇒ Object

After returning from this method, both @refined_old and @refined_new must have been set to reasonable values.



110
111
112
113
# File 'lib/refiner.rb', line 110

def create_refinements(old, new, old_highlights, new_highlights)
  @refined_old = render_refinement('-', RED, old, old_highlights)
  @refined_new = render_refinement('+', GREEN, new, new_highlights)
end

#render_refinement(prefix, color, string, highlights, base_index: 0, highlight_color: '') ⇒ Object



98
99
100
101
102
103
104
105
106
# File 'lib/refiner.rb', line 98

def render_refinement(prefix, color, string, highlights,
                      base_index: 0, highlight_color: '')
  return_me = DiffString.new(prefix, color)
  string.each_char.with_index do |char, index|
    highlight = highlights.include?(index + base_index)
    return_me.add(char, highlight, highlight ? highlight_color : '')
  end
  return return_me.to_s
end

#should_highlight?(old, new) ⇒ Boolean

Returns:

  • (Boolean)


55
56
57
58
59
60
61
62
63
# File 'lib/refiner.rb', line 55

def should_highlight?(old, new)
  return false if old.empty? || new.empty?

  # The 15_000 constant has been determined using the "benchmark"
  # program in our bin/ directory.
  return false if old.length + new.length > 15_000

  return true
end

#try_highlight(old, new) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/refiner.rb', line 65

def try_highlight(old, new)
  old_highlights = Set.new
  new_highlights = Set.new
  if should_highlight?(old, new)
    collect_highlights(Diff::LCS.diff(old, new),
                       old_highlights,
                       new_highlights)

    censor_highlights(old, new, old_highlights, new_highlights)
  end

  return old_highlights, new_highlights
end

#try_highlight_initial_lines(old, new) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/refiner.rb', line 79

def try_highlight_initial_lines(old, new)
  old_line_count = old.lines.count
  new_line_count = new.lines.count
  if old_line_count == new_line_count
    return Set.new, Set.new
  end

  min_line_count = [old_line_count, new_line_count].min
  if min_line_count == 0
    return Set.new, Set.new
  end

  # Truncate old and new so they have the same number of lines
  old = old.lines[0..(min_line_count - 1)].join
  new = new.lines[0..(min_line_count - 1)].join

  return try_highlight(old, new)
end