From 3841563fb248477eb3306742e3132775ec4115dc Mon Sep 17 00:00:00 2001 From: Jason Laster Date: Mon, 6 Aug 2012 15:39:57 -0400 Subject: [PATCH] add support tab-completing arbitrarily long paths --- lib/pry/completion.rb | 142 +++++++++++++++++++++++----------------- test/test_completion.rb | 102 +++++++++++++++++++++++------ 2 files changed, 164 insertions(+), 80 deletions(-) diff --git a/lib/pry/completion.rb b/lib/pry/completion.rb index e64e1463..45b2d061 100644 --- a/lib/pry/completion.rb +++ b/lib/pry/completion.rb @@ -43,34 +43,48 @@ class Pry # @param [Array] commands The array of Pry commands. def self.build_completion_proc(target, pry=nil, commands=[""]) proc do |input| + + # if there are multiple contexts e.g. cd 1/2/3 + # get new target for 1/2 and find candidates for 3 + path, input = build_path(input) + + unless path.call.empty? + target, _ = Pry::Helpers::BaseHelpers.context_from_object_path(path.call, pry) + target = target.last + end + begin bind = target case input + + + # Complete stdlib symbols + when /^(\/[^\/]*\/)\.([^.]*)$/ # Regexp receiver = $1 message = Regexp.quote($2) - candidates = Regexp.instance_methods.collect{|m| m.to_s} - select_message(receiver, message, candidates) + candidates = Regexp.instance_methods.collect(&:to_s) + select_message(path, receiver, message, candidates) when /^([^\]]*\])\.([^.]*)$/ # Array receiver = $1 message = Regexp.quote($2) - candidates = Array.instance_methods.collect{|m| m.to_s} - select_message(receiver, message, candidates) + candidates = Array.instance_methods.collect(&:to_s) + select_message(path, receiver, message, candidates) when /^([^\}]*\})\.([^.]*)$/ # Proc or Hash receiver = $1 message = Regexp.quote($2) - candidates = Proc.instance_methods.collect{|m| m.to_s} - candidates |= Hash.instance_methods.collect{|m| m.to_s} - select_message(receiver, message, candidates) + candidates = Proc.instance_methods.collect(&:to_s) + candidates |= Hash.instance_methods.collect(&:to_s) + select_message(path, receiver, message, candidates) when /^(:[^:.]*)$/ # Symbol @@ -86,16 +100,25 @@ class Pry when /^::([A-Z][^:\.\(]*)$/ # Absolute Constant or class methods receiver = $1 - candidates = Object.constants.collect{|m| m.to_s} + candidates = Object.constants.collect(&:to_s) candidates.grep(/^#{receiver}/).collect{|e| "::" + e} - when /^([A-Z].*)::([^:.]*)$/ + + # Complete target symbols + + when /^([A-Z][A-Za-z0-9]*)$/ + # Constant + message = $1 + candidates = target.eval("self.class.constants").collect(&:to_s) + candidates.grep(/^#{message}/).collect(&path) + + when /^([A-Z].*)::([^:.]*)$/ # Constant or class methods receiver = $1 message = Regexp.quote($2) begin - candidates = eval("#{receiver}.constants.collect{|m| m.to_s}", bind) - candidates |= eval("#{receiver}.methods.collect{|m| m.to_s}", bind) + candidates = eval("#{receiver}.constants.collect(&:to_s)", bind) + candidates |= eval("#{receiver}.methods.collect(&:to_s)", bind) rescue RescuableException candidates = [] end @@ -106,8 +129,8 @@ class Pry receiver = $1 message = Regexp.quote($2) - candidates = Symbol.instance_methods.collect{|m| m.to_s} - select_message(receiver, message, candidates) + candidates = Symbol.instance_methods.collect(&:to_s) + select_message(path, receiver, message, candidates) when /^(-?(0[dbo])?[0-9_]+(\.[0-9_]+)?([eE]-?[0-9]+)?)\.([^.]*)$/ # Numeric @@ -115,42 +138,43 @@ class Pry message = Regexp.quote($5) begin - candidates = eval(receiver, bind).methods.collect{|m| m.to_s} + candidates = eval(receiver, bind).methods.collect(&:to_s) rescue RescuableException candidates = [] end - select_message(receiver, message, candidates) + select_message(path, receiver, message, candidates) - when /^(-?0x[0-9a-fA-F_]+)\.([^.]*)$/ + when /^(-?0x[0-9a-fA-F_]+)\.([^.]*)$/# # Numeric(0xFFFF) receiver = $1 message = Regexp.quote($2) begin - candidates = eval(receiver, bind).methods.collect{|m| m.to_s} + candidates = eval(receiver, bind).methods.collect(&:to_s) rescue RescuableException candidates = [] end - select_message(receiver, message, candidates) + select_message(path, receiver, message, candidates) when /^(\$[^.]*)$/ + # Global variables regmessage = Regexp.new(Regexp.quote($1)) - candidates = global_variables.collect{|m| m.to_s}.grep(regmessage) + candidates = global_variables.collect(&:to_s).grep(regmessage) - when /^([^."].*)\.([^.]*)$/ - # variable + when /^([^."].*)\.([^.]*)$/ + # Variable receiver = $1 message = Regexp.quote($2) - gv = eval("global_variables", bind).collect{|m| m.to_s} - lv = eval("local_variables", bind).collect{|m| m.to_s} - cv = eval("self.class.constants", bind).collect{|m| m.to_s} + gv = eval("global_variables", bind).collect(&:to_s) + lv = eval("local_variables", bind).collect(&:to_s) + cv = eval("self.class.constants", bind).collect(&:to_s) if (gv | lv | cv).include?(receiver) or /^[A-Z]/ =~ receiver && /\./ !~ receiver # foo.func and foo is local var. OR # Foo::Bar.func begin - candidates = eval("#{receiver}.methods", bind).collect{|m| m.to_s} + candidates = eval("#{receiver}.methods", bind).collect(&:to_s) rescue RescuableException candidates = [] end @@ -169,49 +193,35 @@ class Pry # jruby doesn't always provide #instance_methods() on each # object. if m.respond_to?(:instance_methods) - candidates.concat m.instance_methods(false).collect{|x| x.to_s} + candidates.concat m.instance_methods(false).collect(&:to_s) end } candidates.sort! candidates.uniq! end - select_message(receiver, message, candidates) - - when /^(\.{2}\/)(.*)$/ - # previous scope (../) * - stack = pry.binding_stack.dup - stack.pop - if stack.length >= 1 - receiver = $1 - message = $2 - scope = stack[-1] - - ivars = eval("instance_variables", scope) - locals = eval("local_variables", scope) - select_message(receiver, message, ivars + locals) - end + select_message(path, receiver, message, candidates) when /^\.([^.]*)$/ - # unknown(maybe String) - + # Unknown(maybe String) receiver = "" message = Regexp.quote($1) - candidates = String.instance_methods(true).collect{|m| m.to_s} - select_message(receiver, message, candidates) + candidates = String.instance_methods(true).collect(&:to_s) + select_message(path, receiver, message, candidates) else + candidates = eval( "methods | private_methods | local_variables | " \ "self.class.constants | instance_variables", bind - ).collect{|m| m.to_s} + ).collect(&:to_s) if eval("respond_to?(:class_variables)", bind) - candidates += eval("class_variables", bind).collect { |m| m.to_s } + candidates += eval("class_variables", bind).collect(&:to_s) end - - (candidates|ReservedWords|commands).grep(/^#{Regexp.quote(input)}/) + candidates = (candidates|ReservedWords|commands).grep(/^#{Regexp.quote(input)}/) + candidates.collect(&path) end rescue RescuableException [] @@ -219,21 +229,33 @@ class Pry end end - def self.select_message(receiver, message, candidates) + def self.select_message(path, receiver, message, candidates) candidates.grep(/^#{message}/).collect do |e| - if receiver.match(/^\.{2}\//) - receiver + e.to_s - else - case e - when /^[a-zA-Z_]/ - receiver + "." + e - when /^[0-9]/ - when *Operators - #receiver + " " + e - end + case e + when /^[a-zA-Z_]/ + path.call(receiver + "." + e) + when /^[0-9]/ + when *Operators + #receiver + " " + e end end end + + def self.build_path(input) + return proc {|input| input.to_s }, input if input[/\/\./] + + trailing_slash = input[-1] == '/' + contexts = input.chomp('/').split(/\//) + input = contexts[-1] + + path = proc do |input| + p = contexts[0..-2].push(input).join('/') + p += '/' if trailing_slash && !input.nil? + p + end + + return path, input + end end end diff --git a/test/test_completion.rb b/test/test_completion.rb index 50b91ba7..0d0b5192 100644 --- a/test/test_completion.rb +++ b/test/test_completion.rb @@ -1,5 +1,16 @@ require 'helper' +def new_completer(bind, pry=nil) + Pry::InputCompleter.build_completion_proc(Pry.binding_for(bind), pry) +end + +def completer_test(bind, pry=nil, assert_flag=true) + completer = new_completer(bind, pry) + test = proc {|symbol| completer.call(symbol[0..-2]).include?(symbol).should == assert_flag} + return proc {|*symbols| symbols.each(&test) } +end + + describe Pry::InputCompleter do before do @@ -33,33 +44,83 @@ describe Pry::InputCompleter do it 'should complete instance variables' do object = Object.new + # set variables in appropriate scope object.instance_variable_set(:'@name', 'Pry') object.class.send(:class_variable_set, :'@@number', 10) - object.instance_variables.map { |v| v.to_sym } \ - .include?(:'@name').should == true + # check to see if variables are in scope + object.instance_variables. + map { |v| v.to_sym }. + include?(:'@name').should == true - object.class.class_variables.map { |v| v.to_sym } \ - .include?(:'@@number').should == true - - completer = Pry::InputCompleter.build_completion_proc( - Pry.binding_for(object) - ) + object.class.class_variables. + map { |v| v.to_sym }. + include?(:'@@number').should == true # Complete instance variables. - completer.call('@na').include?('@name').should == true - completer.call('@name.down').include?('@name.downcase').should == true + b = Pry.binding_for(object) + completer_test(b).call('@name', '@name.downcase') # Complete class variables. - completer = Pry::InputCompleter.build_completion_proc( - Pry.binding_for(object.class) - ) + b = Pry.binding_for(object.class) + completer_test(b).call('@@number', '@@number.class') - completer.call('@@nu').include?('@@number').should == true - completer.call('@@number.cl').include?('@@number.class').should == true end - it 'should complete previous scope' do + it 'should complete for stdlib symbols' do + + o = Object.new + # Regexp + completer_test(o).call('/foo/.extend') + + # Array + completer_test(o).call('[1].push') + + # Hash + completer_test(o).call('{"a" => "b"}.keys') + + # Proc + completer_test(o).call('{2}.call') + + # Symbol + completer_test(o).call(':symbol.to_s') + + # Absolute Constant + completer_test(o).call('::IndexError') + end + + it 'should complete for target symbols' do + o = Object.new + + # Constant + module Mod + Con = 'Constant' + module Mod2 + end + end + + completer_test(Mod).call('Con') + + # Constants or Class Methods + completer_test(o).call('Mod::Con') + + # Symbol + foo = :symbol + completer_test(o).call(':symbol') + + # Variables + class << o + attr_accessor :foo + end + o.foo = 'bar' + completer_test(binding).call('o.foo') + + # trailing slash + new_completer(Mod).call('Mod2/').include?('Mod2/').should == true + + end + + it 'should complete for arbitrary scopes' do module Bar @barvar = :bar end @@ -74,11 +135,12 @@ describe Pry::InputCompleter do stack.push(Pry.binding_for(Baz)) stack.push(Pry.binding_for(Bar)) - completer = Pry::InputCompleter.build_completion_proc( - Pry.binding_for(Bar), pry - ) + b = Pry.binding_for(Bar) + completer_test(b, pry).call("../@bazvar") + + completer_test(binding, pry).call('/Bar') - completer.call('../@').include?("../@bazvar").should == true end + end