diff options
Diffstat (limited to 'lib/gitlab/diff/expander.rb')
-rw-r--r-- | lib/gitlab/diff/expander.rb | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/lib/gitlab/diff/expander.rb b/lib/gitlab/diff/expander.rb new file mode 100644 index 00000000000..71c28f2e4d3 --- /dev/null +++ b/lib/gitlab/diff/expander.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class Expander + include Gitlab::Utils::StrongMemoize + + UNFOLD_COUNT = 20 + + def initialize(diff_file, position) + @diff_file = diff_file + @blob = diff_file.old_blob + @position = position + @unfold = true + end + + # Returns merged diff lines with required blob lines for presenting. + def diff_lines + strong_memoize(:diff_lines) do + original_diff_lines = @diff_file.diff_lines + + next original_diff_lines unless should_expand? + + merged_diff_with_blob_lines(original_diff_lines) + end + end + + # Returns the extracted lines from the old blob which should be merged + # with the current diff lines. + def blob_lines + strong_memoize(:blob_lines) do + @blob.load_all_data! + + # Blob lines, unlike diffs, doesn't start with an empty line for + # unchanged line, so the parsing and highlighting step gets fuzzy. + line_prefix = ' ' + blob_as_diff_lines = @blob.data.each_line.map { |line| "#{line_prefix}#{line}" } + + lines = Gitlab::Diff::Parser.new.parse(blob_as_diff_lines, diff_file: @diff_file).to_a + + from = from_blob_line - 1 + to = to_blob_line - 1 + + lines[from..to] + end + end + + private + + def should_expand? + return false unless @diff_file.text? + return false if @blob.nil? + return false unless @position.old_line + return false if @diff_file.diff_lines.empty? + return false if @diff_file.line_for_position(@position) + return false unless expanded_line + + true + end + + def merged_diff_with_blob_lines(lines) + match_line = expanded_line + insert_index = bottom? ? -1 : match_line.index + + lines -= [match_line] unless bottom? + + lines.insert(insert_index, *blob_lines_with_new_match) + + # The inserted blob lines have invalid indexes, so we need + # to reindex them. + reindex(lines) + + lines + end + + # Returns 'unchanged' blob lines with recalculated `old_pos` and + # `new_pos` and the recalculated new match line (needed if we for instance + # we unfolded once, but there are still folded lines). + def blob_lines_with_new_match + old_pos = from_blob_line + new_pos = from_blob_line + offset + + new_blob_lines = [] + + blob_lines.each do |line| + new_blob_lines << Gitlab::Diff::Line.new(line.text, line.type, nil, old_pos, new_pos, + parent_file: @diff_file) + + old_pos += 1 + new_pos += 1 + end + + new_blob_lines.unshift(new_match_line) if new_match_line + + new_blob_lines + end + + def bottom? + @position.old_line > @diff_file.diff_lines.last.old_pos + end + + def reindex(lines) + lines.each_with_index { |line, i| line.index = i } + end + + def new_match_line + # The bottom line match addition is already handled on + # Diff::File#diff_lines_for_serializer + return if bottom? + return unless @unfold + + blob_lines_length = blob_lines.length - 1 + + # This will change for bottom scenarios + old_pos = from_blob_line + new_pos = from_blob_line + offset + + old_line_ref = [old_pos, blob_lines_length].join(',') + new_line_ref = [new_pos, blob_lines_length].join(',') + new_match_line_str = "@@ -#{old_line_ref}+#{new_line_ref} @@" + + Gitlab::Diff::Line.new(new_match_line_str, 'match', nil, old_pos, new_pos) + end + + # TODO: use similar approach used on `to_bottom_blob_line` (this will + # change) + def from_blob_line + return old_line_number + 1 if bottom? + + unfold_times = 1 + comment_position = @position.old_line + index_line = line_number_above_match + + while (from = index_line - (UNFOLD_COUNT * unfold_times)) > comment_position + index_line -= 1 + unfold_times += 1 + end + + prev_line_number = line_before_match&.old_pos || 0 + + if from <= prev_line_number + 1 + @unfold = false + from = prev_line_number + 1 + end + + from + end + + def to_blob_line + if bottom? + # Calculates unfolding times based on how many lines between the + # comment position and the bottom match line it has. + from = from_blob_line + comment_position = @position.old_line + unfold_times = ((comment_position - from_blob_line).to_f / (UNFOLD_COUNT + 1)).ceil + from + (UNFOLD_COUNT * unfold_times) + (unfold_times - 1) + else + line_number_above_match + end + end + + def line_number_above_match + old_line_number - 1 + end + + def offset + new_line_number - old_line_number + end + + def old_line_number + expanded_line.old_pos + end + + def new_line_number + expanded_line.new_pos + end + + def line_before_match + index = expanded_line&.index + + @diff_file.diff_lines[index - 1] if index > 0 + end + + # Returns the line which needed to be expanded in order to send a comment + # in `@position`. + def expanded_line + strong_memoize(:expanded_line) do + if bottom? + @diff_file.diff_lines.last + else + @diff_file.diff_lines + .find { |line| line.old_pos > @position.old_line && line.type == 'match' } + end + end + end + end + end +end |