lib/byebug/commands/frame.rb



# encoding: utf-8
module Byebug

  # Mix-in module to assist in command parsing.
  module FrameFunctions
    def c_frame?(frame_no)
      @state.context.frame_binding(frame_no).nil?
    end

    def switch_to_frame(frame_no)
      if frame_no < 0
        abs_frame_no = Context.stack_size + frame_no
      else
        abs_frame_no = frame_no
      end
    end

    def navigate_to_frame(jump_no)
      return if jump_no == 0
      total_jumps, current_jumps, new_pos = jump_no.abs, 0, @state.frame_pos
      step = jump_no/total_jumps
      loop do
        new_pos += step
        return new_pos if new_pos < 0 || new_pos >= Context.stack_size

        next if c_frame?(new_pos)

        current_jumps += 1
        break if current_jumps == total_jumps
      end
      return new_pos
    end

    def adjust_frame(frame_pos, absolute)
      if absolute
        abs_frame_pos = switch_to_frame(frame_pos)
        return errmsg "Can't navigate to c-frame\n" if c_frame?(abs_frame_pos)
      else
        abs_frame_pos = navigate_to_frame(frame_pos)
      end

      return errmsg "Can't navigate beyond the oldest frame\n" if
        abs_frame_pos >= Context.stack_size
      return errmsg "Can't navigate beyond the newest frame\n" if
        abs_frame_pos < 0

      @state.frame_pos = abs_frame_pos
      @state.file = @state.context.frame_file @state.frame_pos
      @state.line = @state.context.frame_line @state.frame_pos
      @state.previous_line = nil
      ListCommand.new(@state).execute
    end

    def get_frame_class(style, pos)
      frame_class = style == :short ? '' : "#{@state.context.frame_class pos}"
      return frame_class == '' ? '' : "#{frame_class}."
    end

    def get_frame_block_and_method(pos)
      frame_deco_regexp = /((?:block(?: \(\d+ levels\))?|rescue) in )?(.+)/
      frame_deco_method = "#{@state.context.frame_method pos}"
      frame_block_and_method = frame_deco_regexp.match(frame_deco_method)[1..2]
      return frame_block_and_method.map{ |x| x.nil? ? '' : x }
    end

    def get_frame_args(style, pos)
      args = @state.context.frame_args pos
      return '' if args.empty?

      locals = @state.context.frame_locals pos if style == :long
      my_args = args.map do |arg|
        case arg[0]
          when :block
            prefix, default = '&', 'block'
          when :rest
            prefix, default = '*', 'args'
          else
            prefix, default = '', nil
        end
        klass = style == :long && arg[1] ? "##{locals[arg[1]].class}" : ''
        "#{prefix}#{arg[1] || default}#{klass}"
      end

      return "(#{my_args.join(', ')})"
    end

    def get_frame_call(prefix, pos)
      frame_block, frame_method = get_frame_block_and_method(pos)
      frame_class = get_frame_class(Command.settings[:callstyle], pos)
      frame_args = get_frame_args(Command.settings[:callstyle], pos)

      call_str = frame_block + frame_class + frame_method + frame_args

      max_call_str_size = Command.settings[:width] - prefix.size
      if call_str.size > max_call_str_size
        call_str = call_str[0..max_call_str_size - 5] + "...)"
      end

      return call_str
    end

    def print_backtrace
      realsize = Context.stack_size
      calcedsize = @state.context.calced_stack_size
      if calcedsize != realsize
        if Byebug.post_mortem?
          stacksize = calcedsize
        else
          errmsg "Byebug's stacksize (#{calcedsize}) should be #{realsize}. " \
                 "This might be a bug in byebug or ruby's debugging API's\n"
          stacksize = realsize
        end
      else
        stacksize = calcedsize
      end
      (0...stacksize).each do |idx|
        print_frame(idx)
      end
    end

    def print_frame(pos, mark_current = true)
      file = @state.context.frame_file pos
      line = @state.context.frame_line pos

      unless Command.settings[:fullpath]
        path_components = file.split(/[\\\/]/)
        if path_components.size > 3
          path_components[0...-3] = '...'
          file = path_components.join(File::ALT_SEPARATOR || File::SEPARATOR)
        end
      end

      if mark_current
        frame_str = (pos == @state.frame_pos) ? '--> ' : '    '
      else
        frame_str = ""
      end
      frame_str += c_frame?(pos) ? ' ͱ-- ' : ''

      frame_str += sprintf "#%-2d ", pos
      frame_str += get_frame_call frame_str, pos
      file_line = "at #{CommandProcessor.canonic_file(file)}:#{line}"
      if frame_str.size + file_line.size + 1 > Command.settings[:width]
        frame_str += "\n      #{file_line}\n"
      else
        frame_str += " #{file_line}\n"
      end

      print frame_str
    end
  end

  # Implements byebug "where" or "backtrace" command.
  class WhereCommand < Command
    def regexp
      /^\s* (?:w(?:here)?|bt|backtrace) \s*$/x
    end

    def execute
      print_backtrace
    end

    class << self
      def names
        %w(where backtrace)
      end

      def description
        %{w[here]|bt|backtrace\tdisplay stack frames

          Print the entire stack frame. Each frame is numbered; the most recent
          frame is 0. A frame number can be referred to in the "frame" command;
          "up" and "down" add or subtract respectively to frame numbers shown.
          The position of the current frame is marked with -->. C-frames hang
          from their most immediate Ruby frame to indicate that they are not
          navigable}
      end
    end
  end

  class UpCommand < Command
    def regexp
      /^\s* u(?:p)? (?:\s+(\S+))? \s*$/x
    end

    def execute
      pos = get_int(@match[1], "Up")
      return unless pos
      adjust_frame(pos, false)
    end

    class << self
      def names
        %w(up)
      end

      def description
        %{up[ count]\tmove to higher frame}
      end
    end
  end

  class DownCommand < Command
    def regexp
      /^\s* down (?:\s+(\S+))? \s*$/x
    end

    def execute
      pos = get_int(@match[1], "Down")
      return unless pos
      adjust_frame(-pos, false)
    end

    class << self
      def names
        %w(down)
      end

      def description
        %{down[ count]\tmove to lower frame}
      end
    end
  end

  class FrameCommand < Command
    def regexp
      /^\s* f(?:rame)? (?:\s+(\S+))? \s*$/x
    end

    def execute
      return print_frame @state.frame_pos unless @match[1]
      return unless pos = get_int(@match[1], "Frame")
      adjust_frame(pos, true)
    end

    class << self
      def names
        %w(frame)
      end

      def description
        %{f[rame][ frame-number]

          Move the current frame to the specified frame number, or the 0 if no
          frame-number has been given.

          A negative number indicates position from the other end, so "frame -1"
          moves to the oldest frame, and "frame 0" moves to the newest frame.

          Without an argument, the command prints the current stack frame. Since
          the current position is redisplayed, it may trigger a resyncronization
          if there is a front end also watching over things.}
      end
    end
  end
end