gitlab-org--gitlab-foss/lib/gitlab/diff/inline_diff.rb

122 lines
3.7 KiB
Ruby

# frozen_string_literal: true
module Gitlab
module Diff
class InlineDiff
# Regex to find a run of deleted lines followed by the same number of added lines
LINE_PAIRS_PATTERN = %r{
# Runs start at the beginning of the string (the first line) or after a space (for an unchanged line)
(?:\A|\s)
# This matches a number of `-`s followed by the same number of `+`s through recursion
(?<del_ins>
-
\g<del_ins>?
\+
)
# Runs end at the end of the string (the last line) or before a space (for an unchanged line)
(?=\s|\z)
}x.freeze
attr_accessor :old_line, :new_line, :offset
def initialize(old_line, new_line, offset: 0)
@old_line = old_line[offset..-1]
@new_line = new_line[offset..-1]
@offset = offset
end
def inline_diffs
# Skip inline diff if empty line was replaced with content
return if old_line == ""
lcp = longest_common_prefix(old_line, new_line)
lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1])
lcp += offset
old_length = old_line.length + offset
new_length = new_line.length + offset
old_diff_range = lcp..(old_length - lcs - 1)
new_diff_range = lcp..(new_length - lcs - 1)
old_diffs = [old_diff_range] if old_diff_range.begin <= old_diff_range.end
new_diffs = [new_diff_range] if new_diff_range.begin <= new_diff_range.end
[old_diffs, new_diffs]
end
class << self
def for_lines(lines)
changed_line_pairs = find_changed_line_pairs(lines)
inline_diffs = []
changed_line_pairs.each do |old_index, new_index|
old_line = lines[old_index]
new_line = lines[new_index]
old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs
inline_diffs[old_index] = old_diffs
inline_diffs[new_index] = new_diffs
end
inline_diffs
end
private
# Finds pairs of old/new line pairs that represent the same line that changed
# rubocop: disable CodeReuse/ActiveRecord
def find_changed_line_pairs(lines)
# Prefixes of all diff lines, indicating their types
# For example: `" - + -+ ---+++ --+ -++"`
line_prefixes = lines.each_with_object(+"") { |line, s| s << (line[0] || ' ') }.gsub(/[^ +-]/, ' ')
changed_line_pairs = []
line_prefixes.scan(LINE_PAIRS_PATTERN) do
# For `"---+++"`, `begin_index == 0`, `end_index == 6`
begin_index, end_index = Regexp.last_match.offset(:del_ins)
# For `"---+++"`, `changed_line_count == 3`
changed_line_count = (end_index - begin_index) / 2
halfway_index = begin_index + changed_line_count
(begin_index...halfway_index).each do |i|
# For `"---+++"`, index 1 maps to 1 + 3 = 4
changed_line_pairs << [i, i + changed_line_count]
end
end
changed_line_pairs
end
# rubocop: enable CodeReuse/ActiveRecord
end
private
def longest_common_prefix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
max_length = [a.length, b.length].max
length = 0
(0..max_length - 1).each do |pos|
old_char = a[pos]
new_char = b[pos]
break if old_char != new_char
length += 1
end
length
end
def longest_common_suffix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
longest_common_prefix(a.reverse, b.reverse)
end
end
end
end