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..b487a71 100644 --- a/lib/pry-stack_explorer/commands.rb +++ b/lib/pry-stack_explorer/commands.rb @@ -20,69 +20,6 @@ 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) @@ -225,7 +162,8 @@ def process 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)}" + frame = PryStackExplorer::Frame.make(target) + output.puts "##{frame_manager.binding_index} #{frame.info(verbose: true)}" end end end @@ -250,18 +188,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 @@ -320,11 +246,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]) + + [ + arrow, + blue(bold frame_no) + ":", + frame_info, + ].join(" ") + end - "#{arw} ##{i} #{memoized_info(i, b, opts[:v])}" + def memoized_frame(index, b) + frame_manager.user[:frame_info] ||= {} + frame_manager.user[:frame_info][index] ||= PryStackExplorer::Frame.make(b) end def offset_frames diff --git a/lib/pry-stack_explorer/frame.rb b/lib/pry-stack_explorer/frame.rb new file mode 100644 index 0000000..3028b0a --- /dev/null +++ b/lib/pry-stack_explorer/frame.rb @@ -0,0 +1,186 @@ +require_relative "frame/it_block" + +module PryStackExplorer + class Frame + attr_reader :b + + def self.make(_binding) + if defined?(RSpec::Core) && _binding.receiver.is_a?(RSpec::Core::ExampleGroup) + ItBlock.new(_binding) + else + new(_binding) + end + 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 + + DESCRIPTION_PATTERN = %r{ + (? + (?: + block\s + (?:\(\d\ levels\)\ )? + ) + (?:in\ ) + )? + (?.*) + }x + + def description + return unless _description + _description.match(DESCRIPTION_PATTERN).named_captures + end + + module T + extend Pry::Helpers::Text + + # Not in Pry yet + def self.faded(text) + "\e[2m#{text}\e[0m" + end + end + + COLOR_SCHEME = { + description: { + context: :default, + method: :green, + }, + signature: { + module: :default, + method: [:blue, :bold], + arguments: :blue, + } + } + + # faded(" | ") + PIPE = "\e[2m | \e[0m" + + def apply_color(string, color = nil, weight = nil) + return unless string + + string = T.public_send(weight, string) if weight + string = T.public_send(color, string) if color + + string + 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 + + @info = _info + @info[!!verbose] + end + + def _info + output = {} + output[:type] = T.faded(type.to_s.ljust(10)) + + output[:full_description] = [ + # description + [ + apply_color(description["context"], nil), + apply_color(description["method"], :green), + ].compact.join(""), + + # signature + [ + apply_color(signature['module'], nil), + apply_color(signature['method'], :blue, :bold), + apply_color(signature['arguments'], :faded), + ].compact.join("") + + ].compact.join(PIPE) + + base = output.values.join("") + + extra_info = " in #{self_clipped} #{path}" + + { + false => base, + true => base + "\n" + extra_info, + } + 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 + + SIGNATURE_PATTERN = / + (?.*?) + (?[#\.].*?) + (?\(.*?\)) + /x + + def signature + return {} unless pry_method + string = self.class.method_signature_with_owner(pry_method) + + # Will match strings like `Module::Module#method(args)` + string.match(SIGNATURE_PATTERN).named_captures + 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 + end +end diff --git a/lib/pry-stack_explorer/frame/it_block.rb b/lib/pry-stack_explorer/frame/it_block.rb new file mode 100644 index 0000000..3f9b7ca --- /dev/null +++ b/lib/pry-stack_explorer/frame/it_block.rb @@ -0,0 +1,59 @@ +module PryStackExplorer + class Frame + class ItBlock < self + def description + if is_method? + pry_method.name + else + it_description + end + end + + # it "does fun things" + def it_description + if metadata[:location] + "it " + metadata[:description]&.inspect + else + "it (anonymous)" + end + end + + def sig + super || metadata[:class_name] + end + + def type + if is_method? + super || "block" + else + "it" + end + end + + # Matches: + # # + # # + INSPECT_REGEXP = %r{ + \#< + (?.+?) + ( + \s" + (?.*?) + " + )? + ( + \s\( + (?.*?) + \) + )? + > + }x + + def metadata + @metadata ||= b.receiver.inspect + .match(INSPECT_REGEXP) + .named_captures.transform_keys(&:to_sym) + end + end + end +end