235 lines
6.7 KiB
Ruby
235 lines
6.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Given a position, calculates which Blob lines should be extracted, treated and
|
|
# injected in the current diff file lines in order to present a "unfolded" diff.
|
|
module Gitlab
|
|
module Diff
|
|
class LinesUnfolder
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
UNFOLD_CONTEXT_SIZE = 3
|
|
|
|
def initialize(diff_file, position)
|
|
@diff_file = diff_file
|
|
@blob = diff_file.old_blob
|
|
@position = position
|
|
@generate_top_match_line = true
|
|
@generate_bottom_match_line = true
|
|
|
|
# These methods update `@generate_top_match_line` and
|
|
# `@generate_bottom_match_line`.
|
|
@from_blob_line = calculate_from_blob_line!
|
|
@to_blob_line = calculate_to_blob_line!
|
|
end
|
|
|
|
# Returns merged diff lines with required blob lines with correct
|
|
# positions.
|
|
def unfolded_diff_lines
|
|
strong_memoize(:unfolded_diff_lines) do
|
|
next unless unfold_required?
|
|
|
|
merged_diff_with_blob_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 lines, unlike diffs, doesn't start with an empty space for
|
|
# unchanged line, so the parsing and highlighting step can get fuzzy
|
|
# without the following change.
|
|
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
|
|
|
|
def unfold_required?
|
|
strong_memoize(:unfold_required) do
|
|
next false unless @diff_file.text?
|
|
next false unless @position.unchanged?
|
|
next false if @diff_file.new_file? || @diff_file.deleted_file?
|
|
next false unless @position.old_line
|
|
# Invalid position (MR import scenario)
|
|
next false if @position.old_line > @blob.lines.size
|
|
next false if @diff_file.diff_lines.empty?
|
|
next false if @diff_file.line_for_position(@position)
|
|
next false unless unfold_line
|
|
|
|
true
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :from_blob_line, :to_blob_line
|
|
|
|
def merged_diff_with_blob_lines
|
|
lines = @diff_file.diff_lines
|
|
match_line = unfold_line
|
|
insert_index = bottom? ? -1 : match_line.index
|
|
|
|
lines -= [match_line] unless bottom?
|
|
|
|
lines.insert(insert_index, *blob_lines_with_matches)
|
|
|
|
# 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_matches
|
|
old_pos = from_blob_line
|
|
new_pos = from_blob_line + offset
|
|
|
|
new_blob_lines = []
|
|
|
|
new_blob_lines.push(top_blob_match_line) if top_blob_match_line
|
|
|
|
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.push(bottom_blob_match_line) if bottom_blob_match_line
|
|
|
|
new_blob_lines
|
|
end
|
|
|
|
def reindex(lines)
|
|
lines.each_with_index { |line, i| line.index = i }
|
|
end
|
|
|
|
def top_blob_match_line
|
|
strong_memoize(:top_blob_match_line) do
|
|
next unless @generate_top_match_line
|
|
|
|
old_pos = from_blob_line
|
|
new_pos = from_blob_line + offset
|
|
|
|
build_match_line(old_pos, new_pos)
|
|
end
|
|
end
|
|
|
|
def bottom_blob_match_line
|
|
strong_memoize(:bottom_blob_match_line) do
|
|
# The bottom line match addition is already handled on
|
|
# Diff::File#diff_lines_for_serializer
|
|
next if bottom?
|
|
next unless @generate_bottom_match_line
|
|
|
|
position = line_after_unfold_position.old_pos
|
|
|
|
old_pos = position
|
|
new_pos = position + offset
|
|
|
|
build_match_line(old_pos, new_pos)
|
|
end
|
|
end
|
|
|
|
def build_match_line(old_pos, new_pos)
|
|
blob_lines_length = blob_lines.length
|
|
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
|
|
|
|
# Returns the first line position that should be extracted
|
|
# from `blob_lines`.
|
|
def calculate_from_blob_line!
|
|
return unless unfold_required?
|
|
|
|
from = comment_position - UNFOLD_CONTEXT_SIZE
|
|
|
|
# There's no line before the match if it's in the top-most
|
|
# position.
|
|
prev_line_number = line_before_unfold_position&.old_pos || 0
|
|
|
|
if from <= prev_line_number + 1
|
|
@generate_top_match_line = false
|
|
from = prev_line_number + 1
|
|
end
|
|
|
|
from
|
|
end
|
|
|
|
# Returns the last line position that should be extracted
|
|
# from `blob_lines`.
|
|
def calculate_to_blob_line!
|
|
return unless unfold_required?
|
|
|
|
to = comment_position + UNFOLD_CONTEXT_SIZE
|
|
|
|
return to if bottom?
|
|
|
|
next_line_number = line_after_unfold_position.old_pos
|
|
|
|
if to >= next_line_number - 1
|
|
@generate_bottom_match_line = false
|
|
to = next_line_number - 1
|
|
end
|
|
|
|
to
|
|
end
|
|
|
|
def offset
|
|
unfold_line.new_pos - unfold_line.old_pos
|
|
end
|
|
|
|
def line_before_unfold_position
|
|
return unless index = unfold_line&.index
|
|
|
|
@diff_file.diff_lines[index - 1] if index > 0
|
|
end
|
|
|
|
def line_after_unfold_position
|
|
return unless index = unfold_line&.index
|
|
|
|
@diff_file.diff_lines[index + 1] if index >= 0
|
|
end
|
|
|
|
def bottom?
|
|
strong_memoize(:bottom) do
|
|
@position.old_line > last_line.old_pos
|
|
end
|
|
end
|
|
|
|
# Returns the line which needed to be expanded in order to send a comment
|
|
# in `@position`.
|
|
def unfold_line
|
|
strong_memoize(:unfold_line) do
|
|
next last_line if bottom?
|
|
|
|
@diff_file.diff_lines.find do |line|
|
|
line.old_pos > comment_position && line.type == 'match'
|
|
end
|
|
end
|
|
end
|
|
|
|
def comment_position
|
|
@position.old_line
|
|
end
|
|
|
|
def last_line
|
|
@diff_file.diff_lines.last
|
|
end
|
|
end
|
|
end
|
|
end
|