diff --git a/lib/pry-stack_explorer.rb b/lib/pry-stack_explorer.rb index 068ab6a..7bff43e 100755 --- a/lib/pry-stack_explorer.rb +++ b/lib/pry-stack_explorer.rb @@ -4,6 +4,7 @@ require "pry" unless defined?(::Pry) require "pry-stack_explorer/version" require "pry-stack_explorer/commands" +require "pry-stack_explorer/frame" require "pry-stack_explorer/frame_manager" require "pry-stack_explorer/when_started_hook" require "binding_of_caller" diff --git a/lib/pry-stack_explorer/commands.rb b/lib/pry-stack_explorer/commands.rb index d75b902..ae388a7 100644 --- a/lib/pry-stack_explorer/commands.rb +++ b/lib/pry-stack_explorer/commands.rb @@ -1,134 +1,4 @@ module PryStackExplorer - module FrameHelpers - private - - # @return [PryStackExplorer::FrameManager] The active frame manager for - # the current `Pry` instance. - def frame_manager - PryStackExplorer.frame_manager(pry_instance) - end - - # @return [Array] All the frame - # managers for the current `Pry` instance. - def frame_managers - PryStackExplorer.frame_managers(pry_instance) - end - - # @return [Boolean] Whether there is a context to return to once - # the current `frame_manager` is popped. - def prior_context_exists? - frame_managers.count > 1 || frame_manager.prior_binding - end - - # Return a description of the frame (binding). - # This is only useful for regular old bindings that have not been - # enhanced by `#of_caller`. - # @param [Binding] b The binding. - # @return [String] A description of the frame (binding). - def frame_description(b) - b_self = b.eval('self') - b_method = b.eval('__method__') - - if b_method && b_method != :__binding__ && b_method != :__binding_impl__ - b_method.to_s - elsif b_self.instance_of?(Module) - "" - elsif b_self.instance_of?(Class) - "" - else - "
" - end - end - - # Return a description of the passed binding object. Accepts an - # optional `verbose` parameter. - # @param [Binding] b The binding. - # @param [Boolean] verbose Whether to generate a verbose description. - # @return [String] The description of the binding. - def frame_info(b, verbose = false) - meth = b.eval('__method__') - b_self = b.eval('self') - meth_obj = Pry::Method.from_binding(b) if meth - - type = b.frame_type ? "[#{b.frame_type}]".ljust(9) : "" - desc = b.frame_description ? "#{b.frame_description}" : "#{frame_description(b)}" - sig = meth_obj ? "<#{signature_with_owner(meth_obj)}>" : "" - - self_clipped = "#{Pry.view_clip(b_self)}" - path = '@ ' + b.source_location.join(':') - - if !verbose - "#{type} #{desc} #{sig}" - else - "#{type} #{desc} #{sig}\n in #{self_clipped} #{path}" - end - end - - # @param [Pry::Method] meth_obj The method object. - # @return [String] Signature for the method object in Class#method format. - def signature_with_owner(meth_obj) - if !meth_obj.undefined? - args = meth_obj.parameters.inject([]) do |arr, (type, name)| - name ||= (type == :block ? 'block' : "arg#{arr.size + 1}") - arr << case type - when :req then name.to_s - when :opt then "#{name}=?" - when :rest then "*#{name}" - when :block then "&#{name}" - else '?' - end - end - "#{meth_obj.name_with_owner}(#{args.join(', ')})" - else - "#{meth_obj.name_with_owner}(UNKNOWN) (undefined method)" - end - end - - # Regexp.new(args[0]) - def find_frame_by_regex(regex, up_or_down) - frame_index = find_frame_by_block(up_or_down) do |b| - b.eval("__method__").to_s =~ regex - end - - if frame_index - frame_index - else - raise Pry::CommandError, "No frame that matches #{regex.source} found!" - end - end - - def find_frame_by_object_regex(class_regex, method_regex, up_or_down) - frame_index = find_frame_by_block(up_or_down) do |b| - class_match = b.eval("self.class").to_s =~ class_regex - meth_match = b.eval("__method__").to_s =~ method_regex - - class_match && meth_match - end - - if frame_index - frame_index - else - raise Pry::CommandError, "No frame that matches #{class_regex.source}" + '#' + "#{method_regex.source} found!" - end - end - - def find_frame_by_block(up_or_down) - start_index = frame_manager.binding_index - - if up_or_down == :down - enum = frame_manager.bindings[0..start_index - 1].reverse_each - else - enum = frame_manager.bindings[start_index + 1..-1] - end - - new_frame = enum.find do |b| - yield(b) - end - - frame_manager.bindings.index(new_frame) - end - end - Commands = Pry::CommandSet.new do create_command "up", "Go up to the caller's context." do @@ -145,20 +15,7 @@ def find_frame_by_block(up_or_down) 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 + go_updown(:up, inc) end end @@ -176,24 +33,7 @@ def process 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 + go_updown(:down, inc) end end @@ -212,21 +52,11 @@ def process BANNER def process - if !frame_manager - raise Pry::CommandError, "nowhere to go!" + if args[0].empty? + frame = PryStackExplorer::Frame.make(target) + output.puts "##{frame_manager.binding_index} #{frame.info(verbose: true)}" 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 + go_updown(:up, args[0]) end end end @@ -250,18 +80,6 @@ def options(opt) 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>] Return tuple of # base_frame_index and the array of frames. def selected_stack_frames @@ -291,9 +109,8 @@ def selected_stack_frames 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):" + stack_size = frame_manager.bindings.size + title = "Showing all accessible frames in stack (#{stack_size} in total):" content = [ bold(title), @@ -320,11 +137,25 @@ def frames_with_indices end end + ARROW = "=>" + EMPTY = " " + # "=> #0 method_name " def make_stack_line(b, i, active) - arw = active ? "=>" : " " + arrow = active ? ARROW : EMPTY + frame_no = i.to_s.rjust(2) + frame_info = memoized_frame(i, b).info(verbose: opts[:v]) - "#{arw} ##{i} #{memoized_info(i, b, opts[:v])}" + [ + arrow, + blue(bold frame_no) + ":", + frame_info, + ].join(" ") + end + + def memoized_frame(index, b) + frame_manager.user[:frame_info] ||= {} + frame_manager.user[:frame_info][index] ||= PryStackExplorer::Frame.make(b) end def offset_frames @@ -335,10 +166,6 @@ def offset_frames end end - def no_stack_available! - output.puts "No caller stack available!" - end - LOCATION_LAMBDA = ->(_binding){ _binding.source_location[0] } def app_frames diff --git a/lib/pry-stack_explorer/frame.rb b/lib/pry-stack_explorer/frame.rb new file mode 100644 index 0000000..b92a922 --- /dev/null +++ b/lib/pry-stack_explorer/frame.rb @@ -0,0 +1,115 @@ +module PryStackExplorer + class Frame + attr_reader :b + + def self.make(_binding) + new(_binding) + end + + def initialize(_binding) + @b = _binding + end + + # Return a description of the frame (binding). + # This is only useful for regular old bindings that have not been + # enhanced by `#of_caller`. + # @return [String] A description of the frame (binding). + def description + return b.frame_description if b.frame_description + + if is_method? + _method.to_s + elsif b.receiver.instance_of?(Module) + "" + elsif b.receiver.instance_of?(Class) + "" + else + "
" + end + end + + + # Produces a string describing the frame + # @param [Options] verbose: Whether to generate a verbose description. + # @return [String] The description of the binding. + def info(verbose: false) + return @info[!!verbose] if @info + + base = "" + base << faded(pretty_type.ljust(9)) + base << " #{description}" + + if sig + base << faded(" | ") + base << sig + end + + @info = { + false => base, + true => base + "\n in #{self_clipped} #{path}", + } + + @info[!!verbose] + end + + def pretty_type + type ? "[#{type}]" : "" + end + + def type + b.frame_type + end + + def _method + @_method ||= b.eval('__method__') + end + + def is_method? + _method && + _method != :__binding__ && + _method != :__binding_impl__ + end + + def self_clipped + Pry.view_clip(b.receiver) + end + + def path + '@ ' + b.source_location.join(':') + end + + def pry_method + Pry::Method.from_binding(b) if _method + end + + def sig + return unless pry_method + self.class.method_signature_with_owner(pry_method) + end + + # @param [Pry::Method] pry_method The method object. + # @return [String] Signature for the method object in Class#method format. + def self.method_signature_with_owner(pry_method) + if pry_method.undefined? + return "#{pry_method.name_with_owner}(UNKNOWN) (undefined method)" + end + + args = pry_method.parameters.inject([]) do |arr, (type, name)| + name ||= (type == :block ? 'block' : "arg#{arr.size + 1}") + arr << case type + when :req then name.to_s + when :opt then "#{name}=?" + when :rest then "*#{name}" + when :block then "&#{name}" + else '?' + end + end + "#{pry_method.name_with_owner}(#{args.join(', ')})" + end + + # Not in Pry yet + def faded(text) + "\e[2m#{text}\e[0m" + end + end +end diff --git a/lib/pry-stack_explorer/frame_helpers.rb b/lib/pry-stack_explorer/frame_helpers.rb new file mode 100644 index 0000000..c6a57da --- /dev/null +++ b/lib/pry-stack_explorer/frame_helpers.rb @@ -0,0 +1,31 @@ +module PryStackExplorer + module FrameHelpers + private + + # @return [PryStackExplorer::FrameManager] The active frame manager for + # the current `Pry` instance. + def frame_manager + PryStackExplorer.frame_manager(pry_instance) || no_stack_available! + end + + # @return [Array] All the frame + # managers for the current `Pry` instance. + def frame_managers + PryStackExplorer.frame_managers(pry_instance) + end + + def go_updown(up_or_down, argument) + if argument =~ /\d+/ + frame_manager.travel(argument.to_i, up_or_down) + elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(argument) + frame_manager.change_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), up_or_down) + elsif argument =~ /^[^-].*$/ + frame_manager.change_frame_by_regex(Regexp.new(argument), up_or_down) + end + end + + def no_stack_available! + raise Pry::CommandError, "No caller stack available!" + end + end +end diff --git a/lib/pry-stack_explorer/frame_manager.rb b/lib/pry-stack_explorer/frame_manager.rb index 0974a11..e9922f7 100644 --- a/lib/pry-stack_explorer/frame_manager.rb +++ b/lib/pry-stack_explorer/frame_manager.rb @@ -65,10 +65,18 @@ def set_binding_index_safely(index) end end + # Travels up or down by `steps`. (Can be positive (up) or negative (down)) + def travel(steps, up_or_down = :up) + steps = {up: 1, down: -1}[up_or_down] * steps + + change_frame_to(binding_index + steps) + end + # Change active frame to the one indexed by `index`. # Note that indexing base is `0` # @param [Fixnum] index The index of the frame. def change_frame_to(index, run_whereami=true) + check_destination_in_bounds!(index) set_binding_index_safely(index) @@ -81,5 +89,73 @@ def change_frame_to(index, run_whereami=true) @pry.run_command "whereami" if run_whereami end + def check_destination_in_bounds!(index) + if index < 0 + raise Pry::CommandError, "At bottom of stack, cannot go further!" + # elsif index > n + end + end + + + def find_frame_by_block(up_or_down) + start_index = binding_index + + if up_or_down == :down + enum = bindings[0..start_index - 1].reverse_each + else + enum = bindings[start_index + 1..-1] + end + + new_frame = enum.find do |b| + yield(b) + end + + bindings.index(new_frame) + end + + def find_frame_by_regex(regex, up_or_down) + frame_index = find_frame_by_block(up_or_down) do |b| + b.eval("__method__").to_s =~ regex + end + + if frame_index + frame_index + else + raise Pry::CommandError, "No frame that matches #{regex.source} found!" + end + end + + def find_frame_by_object_regex(class_regex, method_regex, up_or_down) + frame_index = find_frame_by_block(up_or_down) do |b| + class_match = b.eval("self.class").to_s =~ class_regex + meth_match = b.eval("__method__").to_s =~ method_regex + + class_match && meth_match + end + + if frame_index + frame_index + else + raise Pry::CommandError, "No frame that matches #{class_regex.source}" + '#' + "#{method_regex.source} found!" + end + end + + def change_frame_by_object_regex(*args) + new_frame_index = find_frame_by_object_regex(*args) + change_frame_to new_frame_index + end + + def change_frame_by_regex(*args) + new_frame_index = find_frame_by_regex(*args) + change_frame_to new_frame_index + end + + # @return [Boolean] Whether there is a context to return to once + # the current `frame_manager` is popped. + # + # Currently unused + def prior_context_exists? + count > 1 || prior_binding + end end end