diff --git a/lib/pry.rb b/lib/pry.rb index 13445352..78d1fc39 100644 --- a/lib/pry.rb +++ b/lib/pry.rb @@ -110,8 +110,9 @@ class Pry _pry_.binding_stack.clear throw(:breakout) else - # otherwise just pops a binding - _pry_.binding_stack.pop + # otherwise just pops a binding and stores it as old binding + _pry_.command_state["cd"].old_binding = _pry_.binding_stack.pop + _pry_.command_state["cd"].append = true end end diff --git a/lib/pry/default_commands/cd.rb b/lib/pry/default_commands/cd.rb index 477e5c26..730ced77 100644 --- a/lib/pry/default_commands/cd.rb +++ b/lib/pry/default_commands/cd.rb @@ -9,41 +9,64 @@ class Pry Usage: cd [OPTIONS] [--help] Move into new context (object or scope). As in unix shells use - `cd ..` to go back and `cd /` to return to Pry top-level). - Complex syntax (e.g cd ../@x/y) also supported. + `cd ..` to go back, `cd /` to return to Pry top-level and `cd -` + to toggle between last two scopes). + Complex syntax (e.g `cd ../@x/y`) also supported. e.g: `cd @x` - e.g: `cd .. + e.g: `cd ..` e.g: `cd /` + e.g: `cd -` https://github.com/pry/pry/wiki/State-navigation#wiki-Changing_scope BANNER def process - path = arg_string.split(/\//) - stack = _pry_.binding_stack.dup + # Extract command arguments. Delete blank arguments like " ", but + # don't delete empty strings like "". + path = arg_string.split(/\//).delete_if { |a| a =~ /\A\s+\z/ } + stack = _pry_.binding_stack.dup - # special case when we only get a single "/", return to root - stack = [stack.first] if path.empty? + # Save current state values for the sake of restoring them them later + # (for example, when an exception raised). + old_binding = state.old_binding + append = state.append + + # Special case when we only get a single "/", return to root. + if path.empty? + set_old_binding(stack.last, true) if old_binding + stack = [stack.first] + end path.each do |context| begin case context.chomp when "" + set_old_binding(stack.last, true) stack = [stack.first] when "::" + set_old_binding(stack.last, false) stack.push(TOPLEVEL_BINDING) when "." next when ".." unless stack.size == 1 - stack.pop + set_old_binding(stack.pop, true) + end + when "-" + if state.old_binding + toggle_old_binding(stack, old_binding, append) end else + unless path.length > 1 + set_old_binding(stack.last, false) + end stack.push(Pry.binding_for(stack.last.eval(context))) end rescue RescuableException => e + set_old_binding(old_binding, append) # Restore previous values. + output.puts "Bad object path: #{arg_string.chomp}. Failed trying to resolve: #{context}" output.puts e.inspect return @@ -52,6 +75,43 @@ class Pry _pry_.binding_stack = stack end + + private + + # Toggle old binding value by either appending it to the current stack + # (when `append` is `true`) or setting the new one (when `append` is + # `false`). + # + # @param [Array] stack The current stack of bindings. + # @param [Binding] old_binding The old binding. + # @param [Boolean] append The adjunction flag. + # + # @return [Binding] The new old binding. + def toggle_old_binding(stack, old_binding, append) + if append + stack.push(old_binding) + old_binding = stack[-2] + else + old_binding = stack.pop + end + append = !append + + set_old_binding(old_binding, append) + + old_binding + end + + # Set new old binding and adjunction flag. + # + # @param [Binding] binding The old binding. + # @param [Boolean] append The adjunction flag. + # + # @return [void] + def set_old_binding(binding, append) + state.old_binding = binding + state.append = append + end + end end end diff --git a/lib/pry/pry_instance.rb b/lib/pry/pry_instance.rb index 6a3057fc..fd177f24 100644 --- a/lib/pry/pry_instance.rb +++ b/lib/pry/pry_instance.rb @@ -60,9 +60,9 @@ class Pry def initialize(options={}) refresh(options) - @binding_stack = [] - @indent = Pry::Indent.new - @command_state = {} + @binding_stack = [] + @indent = Pry::Indent.new + @command_state = {} end # Refresh the Pry instance settings from the Pry class. diff --git a/test/test_default_commands/test_cd.rb b/test/test_default_commands/test_cd.rb index f01fcc5d..50a41491 100644 --- a/test/test_default_commands/test_cd.rb +++ b/test/test_default_commands/test_cd.rb @@ -5,6 +5,333 @@ describe 'Pry::DefaultCommands::Cd' do $obj = nil end + describe 'state' do + it 'should not to be set up in fresh instance' do + instance = nil + redirect_pry_io(InputTester.new("cd", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.should == nil + instance.command_state["cd"].append.should == nil + end + end + + describe 'old binding toggling with `cd -`' do + describe 'when an error was raised' do + it 'should ensure cd @ raises SyntaxError' do + mock_pry("cd @").should =~ /SyntaxError/ + end + + it 'should keep correct old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd @", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.should == nil + instance.command_state["cd"].append.should == nil + + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd @", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == TOPLEVEL_BINDING.eval("self") + instance.command_state["cd"].append.should == false + + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd @", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :mon_dogg + instance.command_state["cd"].append.should == true + end + end + + describe 'when using simple cd syntax' do + it 'should keep correct old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == TOPLEVEL_BINDING.eval("self") + instance.command_state["cd"].append.should == false + end + + it 'should toggle with a single `cd -` call' do + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :mon_dogg + instance.command_state["cd"].append.should == true + end + + it 'should toggle with multple `cd -` calls' do + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == TOPLEVEL_BINDING.eval("self") + instance.command_state["cd"].append.should == false + end + end + + describe 'series of cd calls' do + it 'should keep correct old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd 42", "cd :john_dogg", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == 42 + instance.command_state["cd"].append.should == false + end + + it 'should toggle with a single `cd -` call' do + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd 42", "cd :john_dogg", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :john_dogg + instance.command_state["cd"].append.should == true + end + + it 'should toggle with multple `cd -` calls' do + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd 42", "cd :john_dogg", "cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == 42 + instance.command_state["cd"].append.should == false + end + + it 'should toggle with fuzzy `cd -` calls' do + instance = nil + redirect_pry_io(InputTester.new("cd :mon_dogg", "cd -", "cd 42", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == 42 + instance.command_state["cd"].append.should == true + end + end + + describe 'when using cd ..' do + before do + $obj = Object.new + $obj.instance_variable_set(:@x, 66) + $obj.instance_variable_set(:@y, 79) + end + + it 'should keep correct old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd ..", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :john_dogg + instance.command_state["cd"].append.should == true + + redirect_pry_io(InputTester.new("cd :john_dogg", "cd $obj/@x/../@y", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == 66 + instance.command_state["cd"].append.should == true + end + + it 'should toggle with a single `cd -` call' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd ..", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == TOPLEVEL_BINDING.eval("self") + instance.command_state["cd"].append.should == false + + redirect_pry_io(InputTester.new("cd :john_dogg", "cd $obj/@x/../@y", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == 79 + instance.command_state["cd"].append.should == false + end + + it 'should toggle with multiple `cd -` calls' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd ..", "cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :john_dogg + instance.command_state["cd"].append.should == true + + redirect_pry_io(InputTester.new("cd :john_dogg", "cd $obj/@x/../@y", "cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == 66 + instance.command_state["cd"].append.should == true + end + end + + describe 'when using cd ::' do + it 'should keep correct old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd ::", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :john_dogg + instance.command_state["cd"].append.should == false + end + + it 'should toggle with a single `cd -` call' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd ::", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == TOPLEVEL_BINDING.eval("self") + instance.command_state["cd"].append.should == true + end + + it 'should toggle with multiple `cd -` calls' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd ::", "cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :john_dogg + instance.command_state["cd"].append.should == false + end + end + + describe 'when using cd /' do + it 'should keep correct old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd /", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :john_dogg + instance.command_state["cd"].append.should == true + end + + it 'should toggle with a single `cd -` call' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd /", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == TOPLEVEL_BINDING.eval("self") + instance.command_state["cd"].append.should == false + end + + it 'should toggle with multiple `cd -` calls' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd /", "cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :john_dogg + instance.command_state["cd"].append.should == true + end + end + + describe 'when using ^D (Control-D) key press' do + before do + @control_d = "Pry::DEFAULT_CONTROL_D_HANDLER.call('', _pry_)" + end + + it 'should keep correct old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd :mon_dogg", + "cd :kyr_dogg", @control_d, "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :kyr_dogg + instance.command_state["cd"].append.should == true + end + + it 'should toggle with a single `cd -` call' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd :mon_dogg", + "cd :kyr_dogg", @control_d, "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :mon_dogg + instance.command_state["cd"].append.should == false + end + + it 'should toggle with multiple `cd -` calls' do + instance = nil + redirect_pry_io(InputTester.new("cd :john_dogg", "cd :mon_dogg", + "cd :kyr_dogg", @control_d, "cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.eval("self").should == :kyr_dogg + instance.command_state["cd"].append.should == true + end + end + + it 'should not toggle when there is no old binding' do + instance = nil + redirect_pry_io(InputTester.new("cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.should == nil + instance.command_state["cd"].append.should == nil + + redirect_pry_io(InputTester.new("cd -", "cd -", "exit-all")) do + instance = Pry.new + instance.repl + end + + instance.command_state["cd"].old_binding.should == nil + instance.command_state["cd"].append.should == nil + end + end + it 'should cd into simple input' do b = Pry.binding_for(Object.new) b.eval("x = :mon_ouie")