Module: PryStackExplorer

Defined in:
lib/pry-stack_explorer.rb,
lib/pry-stack_explorer/version.rb,
lib/pry-stack_explorer/commands.rb,
lib/pry-stack_explorer/frame_manager.rb,
lib/pry-stack_explorer/when_started_hook.rb

Defined Under Namespace

Modules: FrameHelpers Classes: FrameManager, WhenStartedHook

Constant Summary collapse

VERSION =
'0.6.1'
Commands =
Pry::CommandSet.new do
  create_command "up", "Go up to the caller's context." do
    include FrameHelpers

    banner <<-BANNER
      Usage: up [OPTIONS]
        Go up to the caller's context. Accepts optional numeric parameter for how many frames to move up.
        Also accepts a string (regex) instead of numeric; for jumping to nearest parent method frame which matches the regex.
        e.g: up      #=> Move up 1 stack frame.
        e.g: up 3    #=> Move up 2 stack frames.
        e.g: up meth #=> Jump to nearest parent stack frame whose method matches /meth/ regex, i.e `my_method`.
    BANNER

    def process
      inc = args.first.nil? ? "1" : args.first

      if !frame_manager
        raise Pry::CommandError, "Nowhere to go!"
      else
        if inc =~ /\d+/
          frame_manager.change_frame_to frame_manager.binding_index + inc.to_i
        elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(inc)
          new_frame_index = find_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), :up)
          frame_manager.change_frame_to new_frame_index
        elsif inc =~ /^[^-].*$/
          new_frame_index = find_frame_by_regex(Regexp.new(inc), :up)
          frame_manager.change_frame_to new_frame_index
        end
      end
    end
  end

  create_command "down", "Go down to the callee's context." do
    include FrameHelpers

    banner <<-BANNER
      Usage: down [OPTIONS]
        Go down to the callee's context. Accepts optional numeric parameter for how many frames to move down.
        Also accepts a string (regex) instead of numeric; for jumping to nearest child method frame which matches the regex.
        e.g: down      #=> Move down 1 stack frame.
        e.g: down 3    #=> Move down 2 stack frames.
        e.g: down meth #=> Jump to nearest child stack frame whose method matches /meth/ regex, i.e `my_method`.
    BANNER

    def process
      inc = args.first.nil? ? "1" : args.first

      if !frame_manager
        raise Pry::CommandError, "Nowhere to go!"
      else
        if inc =~ /\d+/
          if frame_manager.binding_index - inc.to_i < 0
            raise Pry::CommandError, "At bottom of stack, cannot go further!"
          else
            frame_manager.change_frame_to frame_manager.binding_index - inc.to_i
          end
        elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(inc)
          new_frame_index = find_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), :down)
          frame_manager.change_frame_to new_frame_index
        elsif inc =~ /^[^-].*$/
          new_frame_index = find_frame_by_regex(Regexp.new(inc), :down)
          frame_manager.change_frame_to new_frame_index
        end
      end
    end
  end

  create_command "frame", "Switch to a particular frame." do
    include FrameHelpers

    banner <<-BANNER
      Usage: frame [OPTIONS]
        Switch to a particular frame. Accepts numeric parameter (or regex for method name) for the target frame to switch to (use with show-stack).
        Negative frame numbers allowed. When given no parameter show information about the current frame.

        e.g: frame 4         #=> jump to the 4th frame
        e.g: frame meth      #=> jump to nearest parent stack frame whose method matches /meth/ regex, i.e `my_method`
        e.g: frame -2        #=> jump to the second-to-last frame
        e.g: frame           #=> show information info about current frame
    BANNER

    def process
      if !frame_manager
        raise Pry::CommandError, "nowhere to go!"
      else

        if args[0] =~ /\d+/
          frame_manager.change_frame_to args[0].to_i
        elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(args[0])
          new_frame_index = find_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), :up)
          frame_manager.change_frame_to new_frame_index
        elsif args[0] =~ /^[^-].*$/
          new_frame_index = find_frame_by_regex(Regexp.new(args[0]), :up)
          frame_manager.change_frame_to new_frame_index
        else
          output.puts "##{frame_manager.binding_index} #{frame_info(target, true)}"
        end
      end
    end
  end

  create_command "stack", "Show all frames" do
    include FrameHelpers

    banner <<-BANNER
      Usage: stack [OPTIONS]
        Show all accessible stack frames.
        e.g: stack -v

        alias: show-stack
    BANNER

    def options(opt)
      opt.on :v, :verbose, "Include extra information."
      opt.on :H, :head, "Display the first N stack frames (defaults to 10).", :optional_argument => true, :as => Integer, :default => 10
      opt.on :T, :tail, "Display the last N stack frames (defaults to 10).", :optional_argument => true, :as => Integer, :default => 10
      opt.on :c, :current, "Display N frames either side of current frame (default to 5).", :optional_argument => true, :as => Integer, :default => 5
      opt.on :a, :app, "Display application frames only", optional_argument: true
    end

    def memoized_info(index, b, verbose)
      frame_manager.user[:frame_info] ||= Hash.new { |h, k| h[k] = [] }

      if verbose
        frame_manager.user[:frame_info][:v][index]      ||= frame_info(b, verbose)
      else
        frame_manager.user[:frame_info][:normal][index] ||= frame_info(b, verbose)
      end
    end

    private :memoized_info

    # @return [Array<Fixnum, Array<Binding>>] Return tuple of
    #   base_frame_index and the array of frames.
    def selected_stack_frames
      if opts.present?(:head)
        [0, frame_manager.bindings[0..(opts[:head] - 1)]]

      elsif opts.present?(:tail)
        tail = opts[:tail]
        if tail > frame_manager.bindings.size
          tail = frame_manager.bindings.size
        end

        base_frame_index = frame_manager.bindings.size - tail
        [base_frame_index, frame_manager.bindings[base_frame_index..-1]]

      elsif opts.present?(:current)
        first_frame_index = frame_manager.binding_index - (opts[:current])
        first_frame_index = 0 if first_frame_index < 0
        last_frame_index = frame_manager.binding_index + (opts[:current])
        [first_frame_index, frame_manager.bindings[first_frame_index..last_frame_index]]

      else
        [0, frame_manager.bindings]
      end
    end

    private :selected_stack_frames

    def process
      return no_stack_available! unless frame_manager

      title = "Showing all accessible frames in stack (#{frame_manager.bindings.size} in total):"

      content = [
        bold(title),
        "---",
        make_stack_lines
      ].join("\n")

      stagger_output content
    end

    private

    def make_stack_lines
      frames_with_indices.map do |b, i|
        make_stack_line(b, i, (i == frame_manager.binding_index))
      end.join("\n")
    end

    def frames_with_indices
      if opts.present?(:app) && defined?(ActiveSupport::BacktraceCleaner)
        app_frames
      else
        offset_frames
      end
    end

    # "=> #0  method_name <Class#method(...)>"
    def make_stack_line(b, i, active)
      arw = active ? "=>" : "  "

      "#{arw} ##{i} #{memoized_info(i, b, opts[:v])}"
    end

    def offset_frames
      base_frame_index, frames = selected_stack_frames

      frames.each_with_index.map do |frame, index|
        [frame, index + base_frame_index]
      end
    end

    def no_stack_available!
      output.puts "No caller stack available!"
    end

    LOCATION_LAMBDA = ->(_binding){ _binding.source_location[0] }

    def app_frames
      locations = frame_manager.bindings.map(&LOCATION_LAMBDA)
      filtered = backtrace_cleaner.clean(locations)

      frame_manager.bindings
        .each_with_index
        .map
        .select do |_binding, _index|
          LOCATION_LAMBDA.call(_binding).in?(filtered)
        end
    end

    # also see Rails::BacktraceCleaner
    def backtrace_cleaner
      @backtrace_cleaner ||= ActiveSupport::BacktraceCleaner.new
    end
  end

  alias_command "show-stack", "stack"

end

Class Method Summary collapse

Class Method Details

.bindings_equal?(b1, b2) ⇒ Boolean

Simple test to check whether two ‘Binding` objects are equal.

Parameters:

  • b1 (Binding)

    First binding.

  • b2 (Binding)

    Second binding.

Returns:

  • (Boolean)

    Whether the ‘Binding`s are equal.



109
110
111
112
113
114
# File 'lib/pry-stack_explorer.rb', line 109

def bindings_equal?(b1, b2)
  (b1.eval('self').equal?(b2.eval('self'))) &&
    (b1.eval('__method__') == b2.eval('__method__')) &&
    (b1.eval('local_variables').map { |v| b1.eval("#{v}") }.equal?(
     b2.eval('local_variables').map { |v| b2.eval("#{v}") }))
end

.clear_frame_managers(_pry_) ⇒ Object Also known as: delete_frame_managers

Clear the stack of frame managers for the Pry instance

Parameters:

  • _pry_ (Pry)

    The Pry instance associated with the frame managers



93
94
95
96
# File 'lib/pry-stack_explorer.rb', line 93

def clear_frame_managers(_pry_)
  pop_frame_manager(_pry_) until frame_managers(_pry_).empty?
  frame_hash.delete(_pry_) # this line should be unnecessary!
end

.create_and_push_frame_manager(bindings, _pry_, options = {}) ⇒ Object

Create a ‘Pry::FrameManager` object and push it onto the frame manager stack for the relevant `pry` instance.

Parameters:

  • bindings (Array)

    The array of bindings (frames)

  • _pry_ (Pry)

    The Pry instance associated with the frame manager



35
36
37
38
39
40
# File 'lib/pry-stack_explorer.rb', line 35

def create_and_push_frame_manager(bindings, _pry_, options={})
  fm = FrameManager.new(bindings, _pry_)
  frame_hash[_pry_].push fm
  push_helper(fm, options)
  fm
end

.frame_hashHash

Returns The hash storing all frames for all Pry instances for the current thread.

Returns:

  • (Hash)

    The hash storing all frames for all Pry instances for the current thread.



19
20
21
# File 'lib/pry-stack_explorer.rb', line 19

def frame_hash
  Thread.current[:__pry_frame_managers__] ||= Hash.new { |h, k| h[k] = [] }
end

.frame_manager(_pry_) ⇒ PryStackExplorer::FrameManager

Returns The currently active frame manager.

Returns:



101
102
103
# File 'lib/pry-stack_explorer.rb', line 101

def frame_manager(_pry_)
  frame_hash[_pry_].last
end

.frame_managers(_pry_) ⇒ Array

Return the complete frame manager stack for the Pry instance

Parameters:

  • _pry_ (Pry)

    The Pry instance associated with the frame managers

Returns:

  • (Array)

    The stack of Pry::FrameManager objections



27
28
29
# File 'lib/pry-stack_explorer.rb', line 27

def frame_managers(_pry_)
  frame_hash[_pry_]
end

.pop_frame_manager(_pry_) ⇒ Pry::FrameManager

Delete the currently active frame manager

Parameters:

  • _pry_ (Pry)

    The Pry instance associated with the frame managers.

Returns:

  • (Pry::FrameManager)

    The popped frame manager.



60
61
62
63
64
65
66
# File 'lib/pry-stack_explorer.rb', line 60

def pop_frame_manager(_pry_)
  return if frame_managers(_pry_).empty?

  popped_fm = frame_managers(_pry_).pop
  pop_helper(popped_fm, _pry_)
  popped_fm
end

.pop_helper(popped_fm, _pry_) ⇒ Object

Restore the Pry instance to operate on the previous binding. Also responsible for restoring Pry instance’s backtrace.

Parameters:

  • popped_fm (Pry::FrameManager)

    The recently popped frame manager.

  • _pry_ (Pry)

    The Pry instance associated with the frame managers.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/pry-stack_explorer.rb', line 72

def pop_helper(popped_fm, _pry_)
  if frame_managers(_pry_).empty?
    if _pry_.binding_stack.empty?
      _pry_.binding_stack.push popped_fm.prior_binding
    else
      _pry_.binding_stack[-1] = popped_fm.prior_binding
    end

    frame_hash.delete(_pry_)
  else
    frame_manager(_pry_).refresh_frame(false)
  end

  # restore backtrace
  _pry_.backtrace = popped_fm.prior_backtrace
end

.push_helper(fm, options = {}) ⇒ Object

Update the Pry instance to operate on the specified frame for the current frame manager.

Parameters:



46
47
48
49
50
51
52
# File 'lib/pry-stack_explorer.rb', line 46

def push_helper(fm, options={})
  options = {
    :initial_frame => 0
  }.merge!(options)

  fm.change_frame_to(options[:initial_frame], false)
end