summaryrefslogtreecommitdiff
path: root/lib/gitlab/diff/expander.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/diff/expander.rb')
-rw-r--r--lib/gitlab/diff/expander.rb198
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