diff --git a/lib/pry/code.rb b/lib/pry/code.rb index 53d88e13..14200f78 100644 --- a/lib/pry/code.rb +++ b/lib/pry/code.rb @@ -1,4 +1,17 @@ class Pry + class << self + def Code(obj) + case obj + when Code + obj + when ::Method, Pry::Method, UnboundMethod, Proc + Code.from_method(obj) + else + Code.new(obj) + end + end + end + class Code class << self # Instantiate a `Code` object containing code loaded from a file or @@ -13,7 +26,7 @@ class Pry if File.readable?(fn) f = File.open(fn, 'r') else - raise CommandError, "Cannot open #{file.inspect} for reading." + raise CommandError, "Cannot open #{fn.inspect} for reading." end end new(f, 1, code_type) @@ -34,6 +47,8 @@ class Pry end end + attr_accessor :code_type + # @param [Array] lines # @param [Fixnum?] (1) start_line # @param [Symbol?] (:ruby) code_type @@ -57,6 +72,8 @@ class Pry alias << push def before(line_num, lines=1) + return self unless line_num + dup.instance_eval do @lines = @lines.select { |l, ln| ln >= line_num - lines && ln < line_num } self @@ -64,13 +81,19 @@ class Pry end def between(start_line, end_line) + return self unless start_line && end_line + start_line -= 1 unless start_line < 0 + end_line -= 1 unless end_line < 0 + dup.instance_eval do - @lines = @lines[(start_line + 1)..(end_line + 1)] + @lines = @lines[start_line..end_line] || [] self end end def around(line_num, lines=1) + return self unless line_num + dup.instance_eval do @lines = @lines.select { |l, ln| ln >= line_num - lines && ln <= line_num + lines } self @@ -78,6 +101,8 @@ class Pry end def after(line_num, lines=1) + return self unless line_num + dup.instance_eval do @lines = @lines.select { |l, ln| ln > line_num && ln <= line_num + lines } self @@ -101,6 +126,14 @@ class Pry end end + def with_indentation(spaces=0) + dup.instance_eval do + @with_indentation = !!spaces + @indentation_num = spaces + self + end + end + # @return [String] def inspect Object.instance_method(:to_s).bind(self).call @@ -134,7 +167,13 @@ class Pry end end - lines.map(&:first).join("\n") + if @with_indentation + lines.each do |l| + l[0] = "#{' ' * @indentation_num}#{l[0]}" + end + end + + lines.map { |l| "#{l.first}\n" }.join end def method_missing(name, *args, &blk) diff --git a/lib/pry/default_commands/shell.rb b/lib/pry/default_commands/shell.rb index ad3deac4..6a14af2f 100644 --- a/lib/pry/default_commands/shell.rb +++ b/lib/pry/default_commands/shell.rb @@ -28,67 +28,93 @@ class Pry _pry_.instance_eval(&Pry::FILE_COMPLETIONS) end end - alias_command "file-mode", "shell-mode" - command "cat", "Show code from a file or Pry's input buffer. Type `cat --help` for more information." do |*args| - start_line = 0 - end_line = -1 - file_name = nil - bt_index = 0 + command_class "cat", "Show code from a file or Pry's input buffer. Type `cat --help` for more information." do + banner <<-USAGE + Usage: cat FILE + cat --ex [STACK_INDEX] + cat --in [INPUT_INDEX_OR_RANGE] - opts = Slop.parse!(args) do |opt| - opt.on :s, :start, "Start line (defaults to start of file)Line 1 is the first line.", true, :as => Integer do |line| - start_line = line - 1 - end + cat is capable of showing part or all of a source file, the context of the + last exception, or an expression from Pry's input history. - opt.on :e, :end, "End line (defaults to end of file). Line -1 is the last line", true, :as => Integer do |line| - end_line = line - 1 - end + cat --ex defaults to showing the lines surrounding the location of the last + exception. Invoking it more than once travels up the exception's backtrace, + and providing a number shows the context of the given index of the backtrace. + USAGE - opt.on :ex, "Show a window of N lines either side of the last exception (defaults to 5).", :optional => true, :as => Integer do |bt_index_arg| - window_size = Pry.config.exception_window_size || 5 - ex = _pry_.last_exception - next if !ex - if bt_index_arg - bt_index = bt_index_arg - else - bt_index = ex.bt_index - end - ex.bt_index = (bt_index + 1) % ex.backtrace.size + def options(opt) + opt.on :ex, "Show the context of the last exception.", :optional => true, :as => Integer + opt.on :i, :in, "Show one or more entries from Pry's expression history.", :optional => true, :as => Range, :default => -5..-1 - ex_file, ex_line = ex.bt_source_location_for(bt_index) - start_line = (ex_line - 1) - window_size - start_line = start_line < 0 ? 0 : start_line - end_line = (ex_line - 1) + window_size - if ex_file && RbxPath.is_core_path?(ex_file) - file_name = RbxPath.convert_path_to_full(ex_file) - else - file_name = ex_file - end - end + opt.on :s, :start, "Starting line (defaults to the first line).", :optional => true, :as => Integer + opt.on :e, :end, "Ending line (defaults to the last line).", :optional => true, :as => Integer + opt.on :l, :'line-numbers', "Show line numbers." + opt.on :t, :type, "The file type for syntax highlighting (e.g., 'ruby' or 'python').", true, :as => Symbol - opt.on :i, :in, "Show entries from Pry's input expression history. Takes an index or range.", :optional => true, :as => Range, :default => -5..-1 - - opt.on :l, "line-numbers", "Show line numbers." - opt.on :t, :type, "The specific file type for syntax higlighting (e.g ruby, python)", true, :as => Symbol opt.on :f, :flood, "Do not use a pager to view text longer than one screen." - opt.on :h, :help, "This message." do - output.puts opt.help + end + + def process + handler = case + when opts.present?(:ex) + method :process_ex + when opts.present?(:in) + method :process_in + else + method :process_file + end + + handler.call do |code| + code.code_type = opts[:type] || :ruby + + code. + between(opts[:start] || 1, opts[:end] || -1). + with_line_numbers(opts.present?(:'line-numbers') || opts.present?(:ex)) end end - next if opts.present?(:help) + def process_ex + window_size = Pry.config.exception_window_size || 5 + ex = _pry_.last_exception - if opts.present?(:ex) - if file_name.nil? - raise CommandError, "No Exception or Exception has no associated file." + raise CommandError, "No exception found." unless ex + + if opts[:ex].nil? + bt_index = ex.bt_index + ex.inc_bt_index + else + bt_index = opts[:ex] end - else - file_name = args.shift + + ex_file, ex_line = ex.bt_source_location_for(bt_index) + + raise CommandError, "The given backtrace level is out of bounds." unless ex_file + + if RbxPath.is_core_path?(ex_file) + ex_file = RbxPath.convert_path_to_full(ex_file) + end + + start_line = ex_line - window_size + start_line = 1 if start_line < 1 + end_line = ex_line + window_size + + header = unindent <<-HEADER + #{text.bold 'Exception:'} #{ex.class}: #{ex.message} + -- + #{text.bold('From:')} #{ex_file} @ line #{ex_line} @ #{text.bold("level: #{bt_index}")} of backtrace (of #{ex.backtrace.size - 1}). + + HEADER + + code = yield(Pry::Code.from_file(ex_file). + between(start_line, end_line). + with_marker(ex_line)) + + render_output("#{header}#{code}", opts) end - if opts.present?(:in) + def process_in normalized_range = absolute_index_range(opts[:i], _pry_.input_array.length) input_items = _pry_.input_array[normalized_range] || [] @@ -98,66 +124,30 @@ class Pry raise CommandError, "No expressions found." end - if opts[:i].is_a?(Range) + if zipped_items.length > 1 contents = "" - zipped_items.each do |i, s| contents << "#{text.bold(i.to_s)}:\n" - - code = syntax_highlight_by_file_type_or_specified(s, nil, :ruby) - - if opts.present?(:'line-numbers') - contents << text.indent(text.with_line_numbers(code, 1), 2) - else - contents << text.indent(code, 2) - end + contents << yield(Pry::Code(s).with_indentation(2)).to_s end else - contents = syntax_highlight_by_file_type_or_specified(zipped_items.first.last, nil, :ruby) + contents = yield(Pry::Code(zipped_items.first.last)) end - else + + render_output(contents, opts) + end + + def process_file + file_name = args.shift + unless file_name - raise CommandError, "Must provide a file name." + raise CommandError, "Must provide a filename, --in, or --ex." end - begin - contents, _, _ = read_between_the_lines(file_name, start_line, end_line) - rescue Errno::ENOENT - raise CommandError, "Could not find file: #{file_name}" - end + code = yield(Pry::Code.from_file(file_name)) - contents = syntax_highlight_by_file_type_or_specified(contents, file_name, opts[:type]) - - if opts.present?(:'line-numbers') - contents = text.with_line_numbers contents, start_line + 1 - end - end - - # add the arrow pointing to line that caused the exception - if opts.present?(:ex) - ex_file, ex_line = _pry_.last_exception.bt_source_location_for(bt_index) - contents = text.with_line_numbers contents, start_line + 1, :bright_red - - contents = contents.lines.each_with_index.map do |line, idx| - l = idx + start_line - if l == (ex_line - 1) - " =>#{line}" - else - " #{line}" - end - end.join - - # header for exceptions - output.puts "\n#{Pry::Helpers::Text.bold('Exception:')} #{_pry_.last_exception.class}: #{_pry_.last_exception.message}\n--" - output.puts "#{Pry::Helpers::Text.bold('From:')} #{ex_file} @ line #{ex_line} @ #{text.bold('level: ')} #{bt_index} of backtrace (of #{_pry_.last_exception.backtrace.size - 1}).\n\n" - end - - set_file_and_dir_locals(file_name) - - if opts.present?(:flood) - output.puts contents - else - stagger_output(contents) + set_file_and_dir_locals(file_name) + render_output(code, opts) end end end diff --git a/lib/pry/helpers/command_helpers.rb b/lib/pry/helpers/command_helpers.rb index 236b5ef6..694c6b5f 100644 --- a/lib/pry/helpers/command_helpers.rb +++ b/lib/pry/helpers/command_helpers.rb @@ -15,6 +15,14 @@ class Pry file.close(opts[:unlink]) end + def render_output(str, opts={}) + if opts[:flood] + output.puts str + else + stagger_output str + end + end + def get_method_or_raise(name, target, opts={}, omit_help=false) meth = Pry::Method.from_str(name, target, opts) diff --git a/lib/pry/pry_instance.rb b/lib/pry/pry_instance.rb index bd1b1cd3..67f0f347 100644 --- a/lib/pry/pry_instance.rb +++ b/lib/pry/pry_instance.rb @@ -15,10 +15,11 @@ class Pry attr_accessor :binding_stack attr_accessor :last_result - attr_accessor :last_exception attr_accessor :last_file attr_accessor :last_dir + attr_reader :last_exception + attr_reader :input_array attr_reader :output_array @@ -228,7 +229,8 @@ class Pry result = set_last_result(target.eval(code, Pry.eval_path, Pry.current_line), target) result rescue RescuableException => e - result = set_last_exception(e, target) + self.last_exception = e + e ensure update_input_history(code) hooks.exec_hook :after_eval, result, self @@ -406,16 +408,18 @@ class Pry end # Set the last exception for a session. - # This method should not need to be invoked directly. - # @param [Exception] ex The exception. - # @param [Binding] target The binding to set `_ex_` on. - def set_last_exception(ex, target) + # @param [Exception] ex + def last_exception=(ex) class << ex attr_accessor :file, :line, :bt_index def bt_source_location_for(index) backtrace[index] =~ /(.*):(\d+)/ [$1, $2.to_i] end + + def inc_bt_index + @bt_index = (@bt_index + 1) % backtrace.size + end end ex.bt_index = 0 @@ -423,8 +427,7 @@ class Pry @last_result_is_exception = true @output_array << ex - - self.last_exception = ex + @last_exception = ex end # Update Pry's internal state after evalling code. diff --git a/test/helper.rb b/test/helper.rb index b30d216c..4f1d8cd0 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -30,23 +30,12 @@ class << Pry end end -class MockPryException - attr_accessor :bt_index - attr_accessor :backtrace - - def initialize(*backtrace) - @backtrace = backtrace - @bt_index = 0 - end - - def message - "mock exception" - end - - def bt_source_location_for(index) - backtrace[index] =~ /(.*):(\d+)/ - [$1, $2.to_i] +def mock_exception(*mock_backtrace) + e = StandardError.new("mock exception") + (class << e; self; end).class_eval do + define_method(:backtrace) { mock_backtrace } end + e end Pry.reset_defaults diff --git a/test/test_default_commands/test_introspection.rb b/test/test_default_commands/test_introspection.rb index 6fdb558c..4bd00a41 100644 --- a/test/test_default_commands/test_introspection.rb +++ b/test/test_default_commands/test_introspection.rb @@ -133,7 +133,7 @@ describe "Pry::DefaultCommands::Introspection" do it 'should start editor on first level of backtrace when --ex used with no argument ' do pry_instance = Pry.new(:input => StringIO.new("edit -n --ex"), :output => StringIO.new) - pry_instance.last_exception = MockPryException.new("a:1", "b:2", "c:3") + pry_instance.last_exception = mock_exception("a:1", "b:2", "c:3") pry_instance.rep(self) @__ex_file__.should == "a" @__ex_line__.should == 1 @@ -141,7 +141,7 @@ describe "Pry::DefaultCommands::Introspection" do it 'should start editor on first level of backtrace when --ex 0 used ' do pry_instance = Pry.new(:input => StringIO.new("edit -n --ex 0"), :output => StringIO.new) - pry_instance.last_exception = MockPryException.new("a:1", "b:2", "c:3") + pry_instance.last_exception = mock_exception("a:1", "b:2", "c:3") pry_instance.rep(self) @__ex_file__.should == "a" @__ex_line__.should == 1 @@ -149,7 +149,7 @@ describe "Pry::DefaultCommands::Introspection" do it 'should start editor on second level of backtrace when --ex 1 used' do pry_instance = Pry.new(:input => StringIO.new("edit -n --ex 1"), :output => StringIO.new) - pry_instance.last_exception = MockPryException.new("a:1", "b:2", "c:3") + pry_instance.last_exception = mock_exception("a:1", "b:2", "c:3") pry_instance.rep(self) @__ex_file__.should == "b" @__ex_line__.should == 2 @@ -157,7 +157,7 @@ describe "Pry::DefaultCommands::Introspection" do it 'should start editor on third level of backtrace when --ex 2 used' do pry_instance = Pry.new(:input => StringIO.new("edit -n --ex 2"), :output => StringIO.new) - pry_instance.last_exception = MockPryException.new("a:1", "b:2", "c:3") + pry_instance.last_exception = mock_exception("a:1", "b:2", "c:3") pry_instance.rep(self) @__ex_file__.should == "c" @__ex_line__.should == 3 @@ -165,7 +165,7 @@ describe "Pry::DefaultCommands::Introspection" do it 'should display error message when backtrace level is out of bounds (using --ex 4)' do pry_instance = Pry.new(:input => StringIO.new("edit -n --ex 4"), :output => str_output = StringIO.new) - pry_instance.last_exception = MockPryException.new("a:1", "b:2", "c:3") + pry_instance.last_exception = mock_exception("a:1", "b:2", "c:3") pry_instance.rep(self) str_output.string.should =~ /Exception has no associated file/ end diff --git a/test/test_default_commands/test_shell.rb b/test/test_default_commands/test_shell.rb index 9544d720..926586f7 100644 --- a/test/test_default_commands/test_shell.rb +++ b/test/test_default_commands/test_shell.rb @@ -5,7 +5,7 @@ describe "Pry::DefaultCommands::Shell" do describe "on receiving a file that does not exist" do it 'should display an error message' do - mock_pry("cat supercalifragilicious66").should =~ /Could not find file/ + mock_pry("cat supercalifragilicious66").should =~ /Cannot open/ end end @@ -65,7 +65,7 @@ describe "Pry::DefaultCommands::Shell" do temp_file do |f| f << "bt number 1" f.flush - pry_instance.last_exception = MockPryException.new("#{f.path}:1", "x", "x") + pry_instance.last_exception = mock_exception("#{f.path}:1", "x", "x") pry_instance.rep(self) end @@ -78,7 +78,7 @@ describe "Pry::DefaultCommands::Shell" do temp_file do |f| f << "bt number 1" f.flush - pry_instance.last_exception = MockPryException.new("#{f.path}:1", "x", "x") + pry_instance.last_exception = mock_exception("#{f.path}:1", "x", "x") pry_instance.rep(self) end @@ -91,31 +91,31 @@ describe "Pry::DefaultCommands::Shell" do temp_file do |f| f << "bt number 2" f.flush - pry_instance.last_exception = MockPryException.new("x", "#{f.path}:1", "x") + pry_instance.last_exception = mock_exception("x", "#{f.path}:1", "x") pry_instance.rep(self) end str_output.string.should =~ /bt number 2/ end - it 'should cat third level of backtrace when --ex 2 used ' do + it 'should cat third level of backtrace when --ex 2 used' do pry_instance = Pry.new(:input => StringIO.new("cat --ex 2"), :output => str_output = StringIO.new) temp_file do |f| f << "bt number 3" f.flush - pry_instance.last_exception = MockPryException.new("x", "x", "#{f.path}:1") + pry_instance.last_exception = mock_exception("x", "x", "#{f.path}:1") pry_instance.rep(self) end str_output.string.should =~ /bt number 3/ end - it 'should show error when backtrace level out of bounds ' do + it 'should show error when backtrace level out of bounds' do pry_instance = Pry.new(:input => StringIO.new("cat --ex 3"), :output => str_output = StringIO.new) - pry_instance.last_exception = MockPryException.new("x", "x", "x") + pry_instance.last_exception = mock_exception("x", "x", "x") pry_instance.rep(self) - str_output.string.should =~ /No Exception or Exception has no associated file/ + str_output.string.should =~ /out of bounds/ end it 'each successive cat --ex should show the next level of backtrace, and going past the final level should return to the first' do @@ -129,7 +129,7 @@ describe "Pry::DefaultCommands::Shell" do pry_instance = Pry.new(:input => StringIO.new("cat --ex\n" * 4), :output => (str_output = StringIO.new)) - pry_instance.last_exception = MockPryException.new(*temp_files.map { |f| "#{f.path}:1" }) + pry_instance.last_exception = mock_exception(*temp_files.map { |f| "#{f.path}:1" }) 3.times do |i| pry_instance.rep(self)