├── .ruby-version ├── lib ├── pry-nav │ ├── cli.rb │ ├── version.rb │ ├── pry_ext.rb │ ├── commands.rb │ ├── pry_remote_ext.rb │ └── tracer.rb └── pry-nav.rb ├── Gemfile ├── .gitignore ├── Rakefile ├── .github └── workflows │ └── main.yml ├── test ├── test_helper.rb └── pry_nav_test.rb ├── pry-nav.gemspec ├── LICENSE ├── CHANGELOG.md └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7 -------------------------------------------------------------------------------- /lib/pry-nav/cli.rb: -------------------------------------------------------------------------------- 1 | require 'pry-nav' 2 | -------------------------------------------------------------------------------- /lib/pry-nav/version.rb: -------------------------------------------------------------------------------- 1 | module PryNav 2 | VERSION = '1.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem "test-unit" 5 | 6 | gem "pry", "~> 0.14.1" 7 | gem "pry-remote" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | vendor/bundle -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | task :default => :test 5 | 6 | Rake::TestTask.new(:test) do |task| 7 | task.libs << 'test' 8 | task.test_files = FileList['test/*_test.rb'] 9 | task.verbose = true 10 | end -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: [push,pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: ['3.0', 2.7, 2.6, 2.5, 2.4, 2.3, 2.2, 2.1] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Ruby ${{ matrix.ruby-version }} 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: ${{ matrix.ruby-version }} 17 | - name: Install dependencies 18 | run: bundle install 19 | - name: Run tests 20 | run: bundle exec rake -------------------------------------------------------------------------------- /lib/pry-nav/pry_ext.rb: -------------------------------------------------------------------------------- 1 | require 'pry' unless defined? Pry 2 | require 'pry-nav/tracer' 3 | 4 | class << Pry 5 | alias start_without_pry_nav start 6 | 7 | def start_with_pry_nav(target = TOPLEVEL_BINDING, options = {}) 8 | old_options = options.reject { |k, _| k == :pry_remote } 9 | 10 | if target.is_a?(Binding) && PryNav.check_file_context(target) 11 | # Wrap the tracer around the usual Pry.start 12 | PryNav::Tracer.new(options).run do 13 | start_without_pry_nav(target, old_options) 14 | end 15 | else 16 | # No need for the tracer unless we have a file context to step through 17 | start_without_pry_nav(target, old_options) 18 | end 19 | end 20 | 21 | alias start start_with_pry_nav 22 | end 23 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'test/unit' 3 | 4 | # lets make sure it works will all of the pry extensions 5 | Bundler.require 6 | 7 | class NavSample 8 | def nested_bind_with_call 9 | Pry.start(binding) 10 | nested_bind_call 11 | puts "root_puts" 12 | end 13 | 14 | def nested_bind_call 15 | puts "nested_puts" 16 | end 17 | end 18 | 19 | # lifted from: 20 | # https://github.com/pry/pry-stack_explorer/blob/e3e6bd202e092712900f0d5f239ee21ab2f32b2b/test/support/input_tester.rb 21 | 22 | class InputTester 23 | def initialize(*actions) 24 | if actions.last.is_a?(Hash) && actions.last.keys == [:history] 25 | @hist = actions.pop[:history] 26 | end 27 | 28 | @orig_actions = actions.dup 29 | @actions = actions 30 | end 31 | 32 | def readline(*) 33 | @actions.shift.tap{ |line| @hist << line if @hist } 34 | end 35 | 36 | def rewind 37 | @actions = @orig_actions.dup 38 | end 39 | end -------------------------------------------------------------------------------- /lib/pry-nav.rb: -------------------------------------------------------------------------------- 1 | require 'pry-nav/version' 2 | require 'pry-nav/pry_ext' 3 | require 'pry-nav/commands' 4 | require 'pry-nav/tracer' 5 | 6 | # Optionally load pry-remote monkey patches 7 | require 'pry-nav/pry_remote_ext' if defined? PryRemote 8 | 9 | module PryNav 10 | TRACE_IGNORE_FILES = Dir[File.join(File.dirname(__FILE__), '**', '*.rb')].map { |f| File.expand_path(f) } 11 | 12 | extend self 13 | 14 | # Checks that a binding is in a local file context. Extracted from 15 | # https://github.com/pry/pry/blob/master/lib/pry/default_commands/context.rb 16 | def check_file_context(target) 17 | file = if target.respond_to?(:source_location) 18 | target.source_location.first 19 | else 20 | target.eval('__FILE__') 21 | end 22 | 23 | file == Pry.eval_path || (file !~ /(\(.*\))|<.*>/ && file != '' && file != '-e') 24 | end 25 | 26 | # Reference to currently running pry-remote server. Used by the tracer. 27 | attr_accessor :current_remote_server 28 | end 29 | -------------------------------------------------------------------------------- /pry-nav.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.expand_path('../lib/pry-nav/version', __FILE__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'pry-nav' 7 | gem.version = PryNav::VERSION 8 | gem.author = 'Gopal Patel' 9 | gem.email = 'nixme@stillhope.com' 10 | gem.license = 'MIT' 11 | gem.homepage = 'https://github.com/nixme/pry-nav' 12 | gem.summary = 'Simple execution navigation for Pry.' 13 | gem.description = "Turn Pry into a primitive debugger. Adds 'step' and 'next' commands to control execution." 14 | 15 | gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 16 | gem.files = `git ls-files`.split("\n") 17 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | gem.require_paths = ['lib'] 19 | 20 | gem.required_ruby_version = '>= 2.1.0' 21 | gem.add_runtime_dependency 'pry', '>= 0.9.10', '< 0.15' 22 | gem.add_development_dependency 'rake' 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT/Expat License 2 | 3 | Copyright (c) 2011 by Gopal Patel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/pry-nav/commands.rb: -------------------------------------------------------------------------------- 1 | require 'pry' unless defined? Pry 2 | 3 | module PryNav 4 | Commands = Pry::CommandSet.new do 5 | block_command 'step', 'Step execution into the next line or method.' do |steps| 6 | check_file_context 7 | breakout_navigation :step, steps 8 | end 9 | 10 | block_command 'next', 'Execute the next line within the same stack frame.' do |lines| 11 | check_file_context 12 | breakout_navigation :next, lines 13 | end 14 | 15 | block_command 'continue', 'Continue program execution and end the Pry session.' do 16 | check_file_context 17 | run 'exit-all' 18 | end 19 | 20 | helpers do 21 | def breakout_navigation(action, times) 22 | pry_instance.binding_stack.clear # Clear the binding stack. 23 | throw( 24 | :breakout_nav, # Break out of the REPL loop and 25 | action: action, # signal the tracer. 26 | times: times, 27 | ) 28 | end 29 | 30 | # Ensures that a command is executed in a local file context. 31 | def check_file_context 32 | unless PryNav.check_file_context(target) 33 | raise Pry::CommandError, 'Cannot find local context. Did you use `binding.pry`?' 34 | end 35 | end 36 | end 37 | end 38 | end 39 | 40 | Pry.commands.import PryNav::Commands 41 | -------------------------------------------------------------------------------- /lib/pry-nav/pry_remote_ext.rb: -------------------------------------------------------------------------------- 1 | require 'pry' unless defined? Pry 2 | require 'pry-remote' 3 | 4 | module PryRemote 5 | class Server 6 | # Override the call to Pry.start to save off current Server, pass a 7 | # pry_remote flag so pry-nav knows this is a remote session, and not kill 8 | # the server right away 9 | def run 10 | if PryNav.current_remote_server 11 | raise 'Already running a pry-remote session!' 12 | else 13 | PryNav.current_remote_server = self 14 | end 15 | 16 | setup 17 | Pry.start( 18 | @object, 19 | input: client.input_proxy, 20 | output: client.output, 21 | pry_remote: true, 22 | ) 23 | end 24 | 25 | # Override to reset our saved global current server session. 26 | alias teardown_without_pry_nav teardown 27 | def teardown_with_pry_nav 28 | teardown_without_pry_nav 29 | PryNav.current_remote_server = nil 30 | end 31 | alias teardown teardown_with_pry_nav 32 | end 33 | end 34 | 35 | # Ensure cleanup when a program finishes without another break. For example, 36 | # 'next' on the last line of a program never hits the tracer proc, and thus 37 | # PryNav::Tracer#run doesn't have a chance to cleanup. 38 | at_exit do 39 | set_trace_func nil 40 | PryNav.current_remote_server.teardown if PryNav.current_remote_server 41 | end 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | * Drop support for ruby < 2.1. 4 | * Support Pry 0.14 5 | * Adding tests 6 | * Fix warning on ruby 2.7 7 | 8 | ## 0.3.0 (2019-04-16) 9 | 10 | * Fix circular require warning. 11 | * Support Pry 0.11 & 0.12 12 | 13 | ## 0.2.4 (2014-06-25) 14 | 15 | * Support Pry 0.10 16 | 17 | ## 0.2.3 (2012-12-26) 18 | 19 | * Safer `alias_method_chain`-style patching of `Pry.start` and 20 | `PryRemote::Server#teardown`. (@benizi) 21 | 22 | ## 0.2.2 (2012-06-14) 23 | 24 | * Upgrade to Pry 0.9.10. (@banister) 25 | 26 | ## 0.2.1 (2012-04-24) 27 | 28 | * Upgrade to Pry 0.9.9. (@banister) 29 | * Fix loading issues using new Pry cli.rb convention. No more explicit 30 | `require 'pry-nav'` should be necessary. (@banister) 31 | 32 | ## 0.2.0 (2012-02-19) 33 | 34 | * Removed single letter aliases for **step**, **next**, and **continue** because 35 | of conflicts with common variable names. 36 | * Update [pry-remote][pry-remote] support for 0.1.1. Older releases of 37 | pry-remote no longer supported. 38 | 39 | 40 | ## 0.1.0 (2012-02-02) 41 | 42 | * MRI 1.8.7 support 43 | * Upgrade to Pry 0.9.8 44 | 45 | 46 | ## 0.0.4 (2011-12-03) 47 | 48 | * Performance improvement, primarily for 1.9.2: Don't trace unless in a file 49 | context. `rails console` or standard `pry` shouldn't experience a slowdown 50 | anymore. 51 | * The overriden `Pry.start` now returns the output of the original method, not a 52 | `PryNav::Tracer` instance. 53 | * For consistency with the other nav commands, **continue** now checks for a 54 | local file context. 55 | 56 | 57 | ## 0.0.3 (2011-12-01) 58 | 59 | * Performance improvement: Don't trace while in the Pry console. Only works in 60 | >= 1.9.3-p0 because 1.9.2 segfaults: http://redmine.ruby-lang.org/issues/3921 61 | * Always cleanup pry-remote DRb server and trace function when a program 62 | ends. Fixes [#1](https://github.com/nixme/pry-nav/issues/1). 63 | * **step** and **next** now check for a local file context. Prevents errors and 64 | infinite loops when called from outside `binding.pry`, e.g. `rails console`. 65 | * More resilient cleanup when [pry-remote][pry-remote] CLI disconnects. 66 | 67 | 68 | ## 0.0.2 (2011-11-30) 69 | 70 | * Rudimentary [pry-remote][pry-remote] support. Still a bit buggy. 71 | * **continue** command as an alias for **exit-all** 72 | 73 | 74 | ## 0.0.1 (2011-11-29) 75 | 76 | * First release. Basic **step** and **next** commands. 77 | 78 | 79 | [pry-remote]: https://github.com/Mon-Ouie/pry-remote 80 | -------------------------------------------------------------------------------- /test/pry_nav_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require ::File.expand_path("test_helper", __dir__) 4 | 5 | class PryNavTest < Test::Unit::TestCase 6 | # removing color makes string matching easier 7 | def setup 8 | Pry.color = false 9 | end 10 | 11 | # lifted from: 12 | # https://github.com/pry/pry-stack_explorer/blob/e3e6bd202e092712900f0d5f239ee21ab2f32b2b/test/support/io_utils.rb 13 | def with_pry_output_captured(new_in, new_out = StringIO.new) 14 | old_in = Pry.input 15 | old_out = Pry.output 16 | 17 | # direct stdout so we can test against `puts` in the method we defined above 18 | old_stdout = $stdout 19 | $stdout = new_out 20 | 21 | Pry.input = new_in 22 | Pry.output = new_out 23 | 24 | begin 25 | yield 26 | ensure 27 | Pry.input = old_in 28 | Pry.output = old_out 29 | $stdout = old_stdout 30 | end 31 | 32 | new_out 33 | end 34 | 35 | # `step` will step into the frames, while `next` keeps the debugging execution within the frame 36 | def test_step 37 | o = NavSample.new 38 | 39 | r = with_pry_output_captured( 40 | InputTester.new( 41 | "step", 42 | 43 | "step", 44 | "step", 45 | 46 | "continue" 47 | ) 48 | ){ o.nested_bind_with_call } 49 | 50 | # initial binding display 51 | assert(r.string.include?("def nested_bind_with_call")) 52 | 53 | # after two steps, we are in the second frame, let's make sure we get there 54 | assert(r.string.include?("def nested_bind_call")) 55 | 56 | assert(/nested_puts\n/ =~ r.string) 57 | assert(/root_puts\n/ =~ r.string) 58 | end 59 | 60 | def test_next 61 | o = NavSample.new 62 | 63 | r = with_pry_output_captured( 64 | InputTester.new( 65 | "next", 66 | "next", 67 | "next", 68 | "continue" 69 | ) 70 | ){ o.nested_bind_with_call } 71 | 72 | assert(r.string.include?("def nested_bind_with_call")) 73 | refute(r.string.include?("def nested_bind_call")) 74 | 75 | assert(/nested_puts\n/ =~ r.string) 76 | assert(/root_puts\n/ =~ r.string) 77 | end 78 | 79 | def test_continue 80 | o = NavSample.new 81 | 82 | r = with_pry_output_captured( 83 | InputTester.new( 84 | "continue", 85 | ) 86 | ){ o.nested_bind_with_call } 87 | 88 | assert(r.string.include?("def nested_bind_with_call")) 89 | refute(r.string.include?("def nested_bind_call")) 90 | 91 | assert(/nested_puts\n/ =~ r.string) 92 | assert(/root_puts\n/ =~ r.string) 93 | end 94 | 95 | def test_file_context 96 | assert(PryNav.check_file_context(binding)) 97 | end 98 | end -------------------------------------------------------------------------------- /lib/pry-nav/tracer.rb: -------------------------------------------------------------------------------- 1 | require 'pry' unless defined? Pry 2 | 3 | module PryNav 4 | class Tracer 5 | def initialize(pry_start_options = {}) 6 | @step_in_lines = -1 # Break after this many lines 7 | @frames_when_stepping = nil # Only break at this frame level 8 | @frames = 0 # Traced stack frame level 9 | @pry_start_options = pry_start_options # Options to use for Pry.start 10 | end 11 | 12 | def run 13 | # For performance, disable any tracers while in the console. 14 | stop 15 | 16 | return_value = nil 17 | command = catch(:breakout_nav) do # Coordinates with PryNav::Commands 18 | return_value = yield 19 | {} # Nothing thrown == no navigational command 20 | end 21 | 22 | # Adjust tracer based on command 23 | if process_command(command) 24 | start 25 | else 26 | if @pry_start_options[:pry_remote] && PryNav.current_remote_server 27 | PryNav.current_remote_server.teardown 28 | end 29 | end 30 | 31 | return_value 32 | end 33 | 34 | def start 35 | set_trace_func method(:tracer).to_proc 36 | end 37 | 38 | def stop 39 | set_trace_func nil 40 | end 41 | 42 | def process_command(command = {}) 43 | times = (command[:times] || 1).to_i 44 | times = 1 if times <= 0 45 | 46 | case command[:action] 47 | when :step 48 | @step_in_lines = times 49 | @frames_when_stepping = nil 50 | true 51 | when :next 52 | @step_in_lines = times 53 | @frames_when_stepping = @frames 54 | true 55 | else 56 | false 57 | end 58 | end 59 | 60 | private 61 | 62 | def tracer(event, file, _line, _id, binding, _klass) 63 | # Ignore traces inside pry-nav code 64 | return if file && TRACE_IGNORE_FILES.include?(File.expand_path(file)) 65 | 66 | case event 67 | when 'line' 68 | # Are we stepping? Or continuing by line ('next') and we're at the right 69 | # frame? Then decrement our line counter cause this line counts. 70 | if !@frames_when_stepping || @frames == @frames_when_stepping 71 | @step_in_lines -= 1 72 | @step_in_lines = -1 if @step_in_lines < 0 73 | 74 | # Did we go up a frame and not break for a 'next' yet? 75 | elsif @frames < @frames_when_stepping 76 | @step_in_lines = 0 # Break right away 77 | end 78 | 79 | # Break on this line? 80 | Pry.start(binding, @pry_start_options) if @step_in_lines.zero? 81 | 82 | when 'call', 'class' 83 | @frames += 1 # Track entering a frame 84 | 85 | when 'return', 'end' 86 | @frames -= 1 # Track leaving a stack frame 87 | @frames = 0 if @frames < 0 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pry-nav [![Ruby](https://github.com/nixme/pry-nav/actions/workflows/main.yml/badge.svg)](https://github.com/nixme/pry-nav/actions/workflows/main.yml) 2 | 3 | _A simple execution control add-on for [Pry][pry]._ 4 | 5 | Compatible with MRI >= 2.1.0, JRuby >= 9.1.3.0. 6 | 7 | Teaches [Pry][pry] about `step`, `next`, and `continue` to create a simple 8 | debugger. 9 | 10 | To use, invoke `pry` normally: 11 | 12 | ```ruby 13 | def some_method 14 | binding.pry # Execution will stop here. 15 | puts 'Hello, World!' # Run 'step' or 'next' in the console to move here. 16 | end 17 | ``` 18 | 19 | When using JRuby, you also need to run it with the `--debug` flag. You can 20 | also add the flag to your `JRUBY_OPTS` environment variable for it to apply 21 | when running any ruby command, but do note that even when not making use of 22 | `pry` this has a big impact on JRuby performance. 23 | 24 | `pry-nav` is not yet thread-safe, so only use in single-threaded environments. 25 | 26 | Rudimentary support for [`pry-remote`][pry-remote] (>= 0.1.1) is also included. 27 | Ensure `pry-remote` is loaded or required before `pry-nav`. For example, in a 28 | `Gemfile`: 29 | 30 | ```ruby 31 | gem 'pry' 32 | gem 'pry-remote' 33 | gem 'pry-nav' 34 | ``` 35 | 36 | Stepping through code often? Add the following shortcuts to `~/.pryrc`: 37 | 38 | ```ruby 39 | Pry.commands.alias_command 'c', 'continue' 40 | Pry.commands.alias_command 's', 'step' 41 | Pry.commands.alias_command 'n', 'next' 42 | ``` 43 | 44 | Please note that debugging functionality is implemented through 45 | [`set_trace_func`][set_trace_func], which imposes a large performance 46 | penalty. 47 | 48 | ## Alternatives 49 | _These work with MRI and pry_ 50 | * [**break**][break] 51 | * [**pry-byebug**][pry-byebug] 52 | 53 | ## Contributors 54 | 55 | * Gopal Patel ([@nixme](https://github.com/nixme)) 56 | * John Mair ([@banister](https://github.com/banister)) 57 | * Conrad Irwin ([@ConradIrwin](https://github.com/ConradIrwin)) 58 | * Benjamin R. Haskell ([@benizi](https://github.com/benizi)) 59 | * Jason R. Clark ([@jasonrclark](https://github.com/jasonrclark)) 60 | * Ivo Anjo ([@ivoanjo](https://github.com/ivoanjo)) 61 | * Michael Bianco ([@iloveitaly](https://github.com/iloveitaly)) 62 | 63 | Patches and bug reports are welcome. Just send a [pull request][pullrequests] or 64 | file an [issue][issues]. [Project changelog][changelog]. 65 | 66 | ## Acknowledgments 67 | 68 | * Ruby stdlib's [debug.rb][debug.rb] 69 | * [@Mon-Ouie][Mon-Ouie]'s [pry_debug][pry_debug] 70 | 71 | [pry]: https://pry.github.io/ 72 | [pry-remote]: https://github.com/Mon-Ouie/pry-remote 73 | [set_trace_func]: http://www.ruby-doc.org/core-1.9.3/Kernel.html#method-i-set_trace_func 74 | [pullrequests]: https://github.com/nixme/pry-nav/pulls 75 | [issues]: https://github.com/nixme/pry-nav/issues 76 | [changelog]: https://github.com/nixme/pry-nav/blob/master/CHANGELOG.md 77 | [debug.rb]: https://github.com/ruby/ruby/blob/trunk/lib/debug.rb 78 | [Mon-Ouie]: https://github.com/Mon-Ouie 79 | [pry_debug]: https://github.com/Mon-Ouie/pry_debug 80 | [pry-byebug]: https://github.com/deivid-rodriguez/pry-byebug 81 | [break]: https://github.com/gsamokovarov/break 82 | --------------------------------------------------------------------------------