155 lines
4.8 KiB
Ruby
155 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module RelativePositioning
|
|
class Mover
|
|
attr_reader :range, :start_position
|
|
|
|
def initialize(start, range)
|
|
@range = range
|
|
@start_position = start
|
|
end
|
|
|
|
def move_to_end(object)
|
|
focus = context(object, ignoring: object)
|
|
max_pos = focus.max_relative_position
|
|
|
|
move_to_range_end(focus, max_pos)
|
|
end
|
|
|
|
def move_to_start(object)
|
|
focus = context(object, ignoring: object)
|
|
min_pos = focus.min_relative_position
|
|
|
|
move_to_range_start(focus, min_pos)
|
|
end
|
|
|
|
def move(object, first, last)
|
|
raise ArgumentError, 'object is required' unless object
|
|
|
|
lhs = context(first, ignoring: object)
|
|
rhs = context(last, ignoring: object)
|
|
focus = context(object)
|
|
range = RelativePositioning.range(lhs, rhs)
|
|
|
|
if range.cover?(focus)
|
|
# Moving a object already within a range is a no-op
|
|
elsif range.open_on_left?
|
|
move_to_range_start(focus, range.rhs.relative_position)
|
|
elsif range.open_on_right?
|
|
move_to_range_end(focus, range.lhs.relative_position)
|
|
else
|
|
pos_left, pos_right = create_space_between(range)
|
|
desired_position = position_between(pos_left, pos_right)
|
|
focus.place_at_position(desired_position, range.lhs)
|
|
end
|
|
end
|
|
|
|
def context(object, ignoring: nil)
|
|
return unless object
|
|
|
|
ItemContext.new(object, range, ignoring: ignoring)
|
|
end
|
|
|
|
private
|
|
|
|
def gap_too_small?(pos_a, pos_b)
|
|
return false unless pos_a && pos_b
|
|
|
|
(pos_a - pos_b).abs < MIN_GAP
|
|
end
|
|
|
|
def move_to_range_end(context, max_pos)
|
|
range_end = range.last + 1
|
|
|
|
new_pos = if max_pos.nil?
|
|
start_position
|
|
elsif gap_too_small?(max_pos, range_end)
|
|
max = context.max_sibling
|
|
max.ignoring = context.object
|
|
max.shift_left
|
|
position_between(max.relative_position, range_end)
|
|
else
|
|
position_between(max_pos, range_end)
|
|
end
|
|
|
|
context.object.relative_position = new_pos
|
|
end
|
|
|
|
def move_to_range_start(context, min_pos)
|
|
range_end = range.first - 1
|
|
|
|
new_pos = if min_pos.nil?
|
|
start_position
|
|
elsif gap_too_small?(min_pos, range_end)
|
|
sib = context.min_sibling
|
|
sib.ignoring = context.object
|
|
sib.shift_right
|
|
position_between(sib.relative_position, range_end)
|
|
else
|
|
position_between(min_pos, range_end)
|
|
end
|
|
|
|
context.object.relative_position = new_pos
|
|
end
|
|
|
|
def create_space_between(range)
|
|
pos_left = range.lhs&.relative_position
|
|
pos_right = range.rhs&.relative_position
|
|
|
|
return [pos_left, pos_right] unless gap_too_small?(pos_left, pos_right)
|
|
|
|
gap = range.rhs.create_space_left
|
|
[pos_left - gap.delta, pos_right]
|
|
rescue NoSpaceLeft
|
|
gap = range.lhs.create_space_right
|
|
[pos_left, pos_right + gap.delta]
|
|
end
|
|
|
|
# This method takes two integer values (positions) and
|
|
# calculates the position between them. The range is huge as
|
|
# the maximum integer value is 2147483647.
|
|
#
|
|
# We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
|
|
#
|
|
# Then we handle one of three cases:
|
|
# - If the gap is too small, we raise NoSpaceLeft
|
|
# - If the gap is larger than MAX_GAP, we place the new position at most
|
|
# IDEAL_DISTANCE from the edge of the gap.
|
|
# - otherwise we place the new position at the midpoint.
|
|
#
|
|
# The new position will always satisfy: pos_before <= midpoint <= pos_after
|
|
#
|
|
# As a precondition, the gap between pos_before and pos_after MUST be >= 2.
|
|
# If the gap is too small, NoSpaceLeft is raised.
|
|
#
|
|
# @raises NoSpaceLeft
|
|
def position_between(pos_before, pos_after)
|
|
pos_before ||= range.first
|
|
pos_after ||= range.last
|
|
|
|
pos_before, pos_after = [pos_before, pos_after].sort
|
|
|
|
gap_width = pos_after - pos_before
|
|
|
|
if gap_too_small?(pos_before, pos_after)
|
|
raise NoSpaceLeft
|
|
elsif gap_width > MAX_GAP
|
|
if pos_before <= range.first
|
|
pos_after - IDEAL_DISTANCE
|
|
elsif pos_after >= range.last
|
|
pos_before + IDEAL_DISTANCE
|
|
else
|
|
midpoint(pos_before, pos_after)
|
|
end
|
|
else
|
|
midpoint(pos_before, pos_after)
|
|
end
|
|
end
|
|
|
|
def midpoint(lower_bound, upper_bound)
|
|
((lower_bound + upper_bound) / 2.0).ceil.clamp(lower_bound, upper_bound - 1)
|
|
end
|
|
end
|
|
end
|
|
end
|