lib/rbs/location.rb



module RBS
  class Location
    attr_reader :buffer
    attr_reader :start_pos
    attr_reader :end_pos

    def initialize(buffer:, start_pos:, end_pos:)
      @buffer = buffer
      @start_pos = start_pos
      @end_pos = end_pos
    end

    def inspect
      "#<#{self.class}:#{self.__id__} @buffer=#{buffer.name}, @pos=#{start_pos}...#{end_pos}, source='#{source.lines.first}', start_line=#{start_line}, start_column=#{start_column}>"
    end

    def name
      buffer.name
    end

    def start_line
      start_loc[0]
    end

    def start_column
      start_loc[1]
    end

    def end_line
      end_loc[0]
    end

    def end_column
      end_loc[1]
    end

    def start_loc
      @start_loc ||= buffer.pos_to_loc(start_pos)
    end

    def end_loc
      @end_loc ||= buffer.pos_to_loc(end_pos)
    end

    def range
      start_pos...end_pos
    end

    def source
      @source ||= buffer.content[start_pos...end_pos] or raise
    end

    def to_s
      "#{name || "-"}:#{start_line}:#{start_column}...#{end_line}:#{end_column}"
    end

    def self.to_string(location, default: "*:*:*...*:*")
      location&.to_s || default
    end

    def ==(other)
      other.is_a?(Location) &&
        other.buffer == buffer &&
        other.start_pos == start_pos &&
        other.end_pos == end_pos
    end

    def +(other)
      if other
        raise "Invalid concat: buffer=#{buffer.name}, other.buffer=#{other.buffer.name}" unless other.buffer == buffer

        self.class.new(buffer: buffer,
                       start_pos: start_pos,
                       end_pos: other.end_pos)
      else
        self
      end
    end

    def concat(*others)
      others.each { |other| self << other }
      self
    end

    def <<(other)
      if other
        raise "Invalid concat: buffer=#{buffer.name}, other.buffer=#{other.buffer.name}" unless other.buffer == buffer
        @end_pos = other.end_pos
        @source = nil
        @end_loc = nil
      end
      self
    end

    def pred?(loc)
      loc.is_a?(Location) &&
        loc.name == name &&
        loc.start_pos == end_pos
    end

    def to_json(state = _ = nil)
      {
        start: {
          line: start_line,
          column: start_column
        },
        end: {
          line: end_line,
          column: end_column
        },
        buffer: {
          name: name&.to_s
        }
      }.to_json(state)
    end

    def with_children(required: {}, optional: {})
      # @type var required: Hash[Symbol, Range[Integer] | Location]
      # @type var optional: Hash[Symbol, Range[Integer] | Location | nil]

      this = WithChildren.new(buffer: buffer, start_pos: start_pos, end_pos: end_pos)

      req = required.transform_values do |value|
        case value
        when Location
          value.range
        else
          value
        end
      end

      opt = optional.transform_values do |value|
        case value
        when Location
          value.range
        else
          value
        end
      end

      this.required_children.merge!(req)
      this.optional_children.merge!(opt)

      this
    end

    class WithChildren < Location
      attr_reader :required_children, :optional_children

      def initialize(buffer:, start_pos:, end_pos:)
        super(buffer: buffer, start_pos: start_pos, end_pos: end_pos)

        @optional_children = {}
        @required_children = {}
      end

      def initialize_copy(from)
        required_children.merge!(from.required_children)
        optional_children.merge!(from.optional_children)
        self
      end

      def [](key)
        case
        when required_children.key?(_ = key)
          range = required_children[_ = key]
          Location.new(buffer: buffer, start_pos: range.begin, end_pos: range.end)
        when optional_children.key?(_ = key)
          range = required_children[_ = key] || optional_children[_ = key]
          if range
            Location.new(buffer: buffer, start_pos: range.begin, end_pos: range.end)
          end
        else
          raise "Unknown key given: `#{key}`"
        end
      end

      def merge_required(hash)
        this = dup

        h = hash.transform_values do |value|
          case value
          when Range
            value
          when Location
            value.range
          else
            raise
          end
        end

        this.required_children.merge!(h)

        this
      end

      def merge_optional(hash)
        this = dup

        h = hash.transform_values do |value|
          case value
          when Range
            value
          when Location
            value.range
          else
            nil
          end
        end

        this.optional_children.merge!(h)

        this
      end
    end
  end
end