Class: Refiner

Inherits:
Object
  • Object
show all
Includes:
Colors, WhitespaceLint
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 WhitespaceLint

#add_line_highlights, #collect_ws_highlights

Methods included from Colors

#reversed, #uncolor

Constructor Details

#initialize(old, new) ⇒ Refiner

Returns a new instance of Refiner.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/refiner.rb', line 174

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

  whitespace_highlights = collect_ws_highlights(new)

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

Instance Attribute Details

#refined_newObject (readonly)

Returns the value of attribute refined_new.



24
25
26
# File 'lib/refiner.rb', line 24

def refined_new
  @refined_new
end

#refined_oldObject (readonly)

Returns the value of attribute refined_old.



23
24
25
# File 'lib/refiner.rb', line 23

def refined_old
  @refined_old
end

Instance Method Details

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



46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/refiner.rb', line 46

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



31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/refiner.rb', line 31

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 action: <#{action}>")
      end
    end
  end
end

#create_one_to_many_refinements(old, new, old_highlights, new_highlights, whitespace_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



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/refiner.rb', line 137

def create_one_to_many_refinements(old, new,
                                   old_highlights, new_highlights,
                                   whitespace_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,
                      ws: whitespace_highlights)

  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,
                                              ws: whitespace_highlights)

  @refined_new = refined_line_1 + refined_remaining_lines

  return true
end

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

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



124
125
126
127
128
129
130
131
# File 'lib/refiner.rb', line 124

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

#render_refinement(prefix, base_color, string, highlights, base_index: 0, highlight_color: '', ws: nil) ⇒ Object

ws: a set containing the whitespace errors we want to highlight



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/refiner.rb', line 103

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

    if !ws.nil? && ws.include?(index + base_index)
      # Highlight whitespace error in inverse red
      color = RED
      highlight = true
    end

    return_me.add(char, highlight, color)
  end
  return return_me.to_s
end

#should_highlight?(old, new) ⇒ Boolean

Returns:

  • (Boolean)


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

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



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/refiner.rb', line 69

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



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/refiner.rb', line 83

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