lib/byebug/commands/list.rb



# frozen_string_literal: true

require_relative "../command"
require_relative "../source_file_formatter"
require_relative "../helpers/file"
require_relative "../helpers/parse"

module Byebug
  #
  # List parts of the source code.
  #
  class ListCommand < Command
    include Helpers::FileHelper
    include Helpers::ParseHelper

    self.allow_in_post_mortem = true

    def self.regexp
      /^\s* l(?:ist)? (?:\s*([-=])|\s+(\S+))? \s*$/x
    end

    def self.description
      <<-DESCRIPTION
        l[ist][[-=]][ nn-mm]

        #{short_description}

        Lists lines forward from current line or from the place where code was
        last listed. If "list-" is specified, lists backwards instead. If
        "list=" is specified, lists from current line regardless of where code
        was last listed. A line range can also be specified to list specific
        sections of code.
      DESCRIPTION
    end

    def self.short_description
      "Lists lines of source code"
    end

    def execute
      msg = "No sourcefile available for #{frame.file}"
      raise(msg) unless File.exist?(frame.file)

      b, e = range(@match[2])

      display_lines(b, e)

      processor.prev_line = b
    end

    private

    #
    # Line range to be printed by `list`.
    #
    # If <input> is set, range is parsed from it.
    #
    # Otherwise it's automatically chosen.
    #
    def range(input)
      return auto_range(@match[1] || "+") unless input

      b, e = parse_range(input)
      raise("Invalid line range") unless valid_range?(b, e)

      [b, e]
    end

    def valid_range?(first, last)
      first <= last && (1..max_line).cover?(first) && (1..max_line).cover?(last)
    end

    #
    # Set line range to be printed by list
    #
    # @return first line number to list
    # @return last line number to list
    #
    def auto_range(direction)
      prev_line = processor.prev_line

      if direction == "=" || prev_line.nil?
        source_file_formatter.range_around(frame.line)
      else
        source_file_formatter.range_from(move(prev_line, size, direction))
      end
    end

    def parse_range(input)
      first, err = get_int(lower_bound(input), "List", 1, max_line)
      raise(err) unless first

      if upper_bound(input)
        last, err = get_int(upper_bound(input), "List", 1, max_line)
        raise(err) unless last

        last = amend_final(last)
      else
        first -= (size / 2)
      end

      [first, last || move(first, size - 1)]
    end

    def move(line, size, direction = "+")
      line.send(direction, size)
    end

    #
    # Show a range of lines in the current file.
    #
    # @param min [Integer] Lower bound
    # @param max [Integer] Upper bound
    #
    def display_lines(min, max)
      puts "\n[#{min}, #{max}] in #{frame.file}"

      puts source_file_formatter.lines(min, max).join
    end

    #
    # @param range [String] A string with an integer range format
    #
    # @return [String] The lower bound of the given range
    #
    def lower_bound(range)
      split_range(range)[0]
    end

    #
    # @param range [String] A string with an integer range format
    #
    # @return [String] The upper bound of the given range
    #
    def upper_bound(range)
      split_range(range)[1]
    end

    #
    # @param str [String] A string with an integer range format
    #
    # @return [Array] The upper & lower bounds of the given range
    #
    def split_range(str)
      str.split(/[-,]/)
    end

    extend Forwardable

    def_delegators :source_file_formatter, :amend_final, :size, :max_line

    def source_file_formatter
      @source_file_formatter ||= SourceFileFormatter.new(
        frame.file,
        ->(n) { n == frame.line ? "=>" : "  " }
      )
    end
  end
end