├── lib ├── bond │ ├── version.rb │ ├── completions │ │ ├── struct.rb │ │ ├── array.rb │ │ ├── hash.rb │ │ ├── bond.rb │ │ ├── kernel.rb │ │ ├── activerecord.rb │ │ ├── module.rb │ │ └── object.rb │ ├── readlines │ │ ├── ruby.rb │ │ ├── jruby.rb │ │ └── rawline.rb │ ├── missions │ │ ├── anywhere_mission.rb │ │ ├── default_mission.rb │ │ ├── operator_method_mission.rb │ │ ├── object_mission.rb │ │ └── method_mission.rb │ ├── completion.rb │ ├── input.rb │ ├── readline.rb │ ├── rc.rb │ ├── search.rb │ ├── agent.rb │ ├── m.rb │ └── mission.rb └── bond.rb ├── CONTRIBUTING.md ├── .travis.yml ├── ext └── readline_line_buffer │ ├── readline_line_buffer.c │ └── extconf.rb ├── Rakefile ├── test ├── anywhere_mission_test.rb ├── m_test.rb ├── test_helper.rb ├── mission_test.rb ├── object_mission_test.rb ├── operator_method_mission_test.rb ├── completions_test.rb ├── bond_test.rb ├── search_test.rb ├── completion_test.rb ├── method_mission_test.rb └── agent_test.rb ├── LICENSE.txt ├── .gemspec ├── CHANGELOG.rdoc └── README.rdoc /lib/bond/version.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | VERSION = '0.5.1' 3 | end 4 | -------------------------------------------------------------------------------- /lib/bond/completions/struct.rb: -------------------------------------------------------------------------------- 1 | complete(:method=>"Struct#[]") {|e| e.object.members } -------------------------------------------------------------------------------- /lib/bond/completions/array.rb: -------------------------------------------------------------------------------- 1 | complete(:methods=>%w{delete index rindex include?}, :class=>"Array#") {|e| e.object } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for trying out this project! [See here for contribution guidelines.](http://tagaholic.me/contributing.html) 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: bundle init --gemspec=.gemspec 2 | script: bacon -q -Ilib -I. test/*_test.rb 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - 2.0.0 8 | - 2.1.0 9 | - rbx 10 | - jruby 11 | -------------------------------------------------------------------------------- /lib/bond/completions/hash.rb: -------------------------------------------------------------------------------- 1 | complete(:methods=>%w{delete fetch store, [] has_key? key? include? member? values_at}, 2 | :class=>"Hash#") {|e| e.object.keys } 3 | complete(:methods=>%w{index value? has_value?}, :class=>"Hash#") {|e| e.object.values } -------------------------------------------------------------------------------- /lib/bond/completions/bond.rb: -------------------------------------------------------------------------------- 1 | complete(:methods=>%w{Bond.complete Bond.recomplete}) { 2 | ["on", "method", "methods", "class", "object", "anywhere", "prefix", "search", "action", "place", "name"] 3 | } 4 | complete(:methods=>['Bond.start', 'Bond.restart']) { 5 | %w{gems readline default_mission default_search eval_binding debug eval_debug bare} 6 | } 7 | -------------------------------------------------------------------------------- /lib/bond/readlines/ruby.rb: -------------------------------------------------------------------------------- 1 | # A pure ruby readline which requires {rb-readline}[https://github.com/luislavena/rb-readline]. 2 | class Bond::Ruby < Bond::Readline 3 | def self.readline_setup 4 | require 'readline' 5 | rescue LoadError 6 | abort "Bond Error: rb-readline gem is required for this readline plugin" + 7 | " -> gem install rb-readline" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bond/readlines/jruby.rb: -------------------------------------------------------------------------------- 1 | # Readline for Jruby 2 | class Bond::Jruby < Bond::Readline 3 | def self.readline_setup 4 | require 'readline' 5 | require 'jruby' 6 | class << Readline 7 | ReadlineExt = org.jruby.ext.readline.Readline 8 | def line_buffer 9 | ReadlineExt.s_get_line_buffer(JRuby.runtime.current_context, JRuby.reference(self)) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/bond/readlines/rawline.rb: -------------------------------------------------------------------------------- 1 | # A pure ruby readline which requires {rawline}[http://github.com/h3rald/rawline]. 2 | class Bond::Rawline < Bond::Readline 3 | def self.setup(agent) 4 | require 'rawline' 5 | Rawline.completion_append_character = nil 6 | Rawline.basic_word_break_characters= " \t\n\"\\'`><;|&{(" 7 | Rawline.completion_proc = agent 8 | rescue LoadError 9 | abort "Bond Error: rawline gem is required for this readline plugin -> gem install rawline" 10 | end 11 | 12 | def self.line_buffer 13 | Rawline.editor.line.text 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/bond/missions/anywhere_mission.rb: -------------------------------------------------------------------------------- 1 | # A mission which completes anywhere i.e. even after non word break characters 2 | # such as '[' or '}'. With options :prefix and :anywhere, this mission matches 3 | # on the following regexp condition /:prefix?(:anywhere)$/ and passes the first 4 | # capture group to the mission action. 5 | class Bond::AnywhereMission < Bond::Mission 6 | def initialize(options={}) #@private 7 | options[:on] = Regexp.new("#{options[:prefix]}(#{options[:anywhere]})$") 8 | super 9 | end 10 | 11 | def after_match(input) #@private 12 | @completion_prefix = input.to_s.sub(/#{Regexp.escape(@matched[1])}$/, '') 13 | create_input @matched[1] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /ext/readline_line_buffer/readline_line_buffer.c: -------------------------------------------------------------------------------- 1 | /* readline.c -- GNU Readline module 2 | Copyright (C) 1997-2001 Shugo Maeda */ 3 | /* body of line_buffer() from irb enhancements at http://www.creo.hu/~csaba/ruby/ */ 4 | 5 | #ifdef HAVE_READLINE_READLINE_H 6 | #include "ruby.h" 7 | #include 8 | #include 9 | #include 10 | 11 | static VALUE line_buffer(VALUE self) 12 | { 13 | rb_secure(4); 14 | if (rl_line_buffer == NULL) 15 | return Qnil; 16 | return rb_tainted_str_new2(rl_line_buffer); 17 | } 18 | 19 | void Init_readline_line_buffer() { 20 | VALUE c = rb_cObject; 21 | c = rb_const_get(c, rb_intern("Readline")); 22 | rb_define_singleton_method(c, "line_buffer", (VALUE(*)(ANYARGS))line_buffer, -1); 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /lib/bond/completions/kernel.rb: -------------------------------------------------------------------------------- 1 | complete(:methods=>%w{Kernel#raise Kernel#fail}) { objects_of(Class).select {|e| e < StandardError } } 2 | complete(:methods=>%w{Kernel#system Kernel#exec}) {|e| 3 | ENV['PATH'].split(File::PATH_SEPARATOR).uniq.map {|e| 4 | File.directory?(e) ? Dir.entries(e) : [] 5 | }.flatten.uniq - ['.', '..'] 6 | } 7 | complete(:method=>"Kernel#require", :search=>:files) { 8 | paths = $:.map {|e| Dir["#{e}/**/*.{rb,bundle,dll,so}"].map {|f| f.sub(e+'/', '') } }.flatten 9 | if Object.const_defined?(:Gem) 10 | paths += Gem.path.map {|e| Dir["#{e}/gems/*/lib/*.{rb,bundle,dll,so}"]. 11 | map {|f| f.sub(/^.*\//,'') } }.flatten 12 | end 13 | paths.uniq 14 | } 15 | complete(:methods=>%w{Kernel#trace_var Kernel#untrace_var}) { global_variables.map {|e| ":#{e}"} } -------------------------------------------------------------------------------- /lib/bond/completion.rb: -------------------------------------------------------------------------------- 1 | # any object's methods 2 | complete :object=>"Object" 3 | # method arguments 4 | complete :all_methods=>true 5 | complete :all_operator_methods=>true 6 | # classes and constants 7 | complete(:name=>:constants, :anywhere=>'([A-Z][^. \(]*)::([^: .]*)') {|e| 8 | receiver = e.matched[2] 9 | candidates = eval("#{receiver}.constants | #{receiver}.methods") || [] 10 | normal_search(e.matched[3], candidates).map {|e| "#{receiver}::#{e}" } 11 | } 12 | # absolute constants 13 | complete(:prefix=>'::', :anywhere=>'[A-Z][^:\.\(]*') {|e| Object.constants } 14 | complete(:anywhere=>':[^:\s.]*') {|e| Symbol.all_symbols.map {|f| ":#{f}" } rescue [] } 15 | complete(:anywhere=>'\$[^\s.]*') {|e| global_variables } 16 | complete(:name=>:quoted_files, :on=>/[\s(]["']([^'"]*)$/, :search=>false, :place=>:last) {|e| files(e.matched[1]) } 17 | -------------------------------------------------------------------------------- /lib/bond/completions/activerecord.rb: -------------------------------------------------------------------------------- 1 | attribute_imethods = %w{attribute_for_inspect column_for_attribute decrement} + 2 | %w{increment update_attribute toggle update_attributes []} 3 | complete(:methods=>attribute_imethods, :class=>"ActiveRecord::Base#") {|e| e.object.attribute_names } 4 | 5 | attribute_cmethods = %w{attr_accessible attr_protected attr_readonly create create! decrement_counter} + 6 | %w{destroy_all exists? increment_counter new serialize update update_all update_counters where} 7 | complete(:methods=>attribute_cmethods, :class=>'ActiveRecord::Base.') {|e| e.object.column_names } 8 | 9 | complete(:method=>"ActiveRecord::Base.establish_connection") { %w{adapter host username password database} } 10 | complete(:methods=>%w{find all first last}, :class=>'ActiveRecord::Base.') { 11 | %w{conditions order group having limit offset joins include select from readonly lock} 12 | } -------------------------------------------------------------------------------- /ext/readline_line_buffer/extconf.rb: -------------------------------------------------------------------------------- 1 | # placate rubygems when running `make install` 2 | def dummy_makefile 3 | File.open(File.join(File.dirname(__FILE__), "Makefile"), "w") {|f| 4 | f.puts %[install:\n\techo "This is a dummy extension"] 5 | } 6 | end 7 | 8 | if RUBY_VERSION >= '1.9.2' || RUBY_PLATFORM[/java|mswin|mingw|bccwin|wince/i] || 9 | ARGV.include?('--without-readline') 10 | dummy_makefile 11 | else 12 | require "mkmf" 13 | dir_config("readline") 14 | have_library('readline') 15 | 16 | if !have_header('readline/readline.h') 17 | abort "\n** Bond Install Error: Unable to find readline.h. Please try again. **\n"+ 18 | "To install with your readline: gem install bond -- --with-readline-dir=/path/to/readline\n"+ 19 | "To install without readline: gem install bond -- --without-readline" 20 | else 21 | create_makefile 'readline_line_buffer' 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/bond/completions/module.rb: -------------------------------------------------------------------------------- 1 | complete(:methods=>%w{const_get const_set const_defined? remove_const}, :class=>"Module#") {|e| e.object.constants } 2 | complete(:methods=>%w{class_variable_defined? class_variable_get class_variable_set remove_class_variable}, 3 | :class=>"Module#") {|e| e.object.class_variables } 4 | complete(:methods=>%w{instance_method method_defined? module_function remove_method undef_method}, 5 | :class=>"Module#") {|e| e.object.instance_methods } 6 | complete(:method=>"Module#public") {|e| e.object.private_instance_methods + e.object.protected_instance_methods } 7 | complete(:method=>"Module#private") {|e| e.object.instance_methods + e.object.protected_instance_methods } 8 | complete(:method=>"Module#protected") {|e| e.object.instance_methods + e.object.private_instance_methods } 9 | complete(:methods=>%w{< <= <=> > >= include? include}, :class=>"Module#", :search=>:modules) { objects_of(Module) } 10 | complete(:method=>'Module#alias_method') {|e| e.argument > 1 ? e.object.instance_methods : [] } -------------------------------------------------------------------------------- /lib/bond/missions/default_mission.rb: -------------------------------------------------------------------------------- 1 | # This is the mission called when none of the others match. 2 | class Bond::DefaultMission < Bond::Mission 3 | ReservedWords = [ 4 | "BEGIN", "END", "alias", "and", "begin", "break", "case", "class", "def", "defined?", "do", "else", "elsif", "end", "ensure", 5 | "false", "for", "if", "in", "module", "next", "nil", "not", "or", "redo", "rescue", "retry", "return", "self", "super", 6 | "then", "true", "undef", "unless", "until", "when", "while", "yield" 7 | ] 8 | 9 | 10 | # Default action which generates methods, private methods, reserved words, local variables and constants. 11 | def self.completions(input=nil) 12 | Bond::Mission.current_eval("methods | private_methods | local_variables | " + 13 | "self.class.constants | instance_variables") | ReservedWords 14 | end 15 | 16 | def initialize(options={}) #@private 17 | options[:action] ||= self.class.method(:completions) 18 | super 19 | end 20 | def default_on; end #@private 21 | end 22 | -------------------------------------------------------------------------------- /lib/bond/completions/object.rb: -------------------------------------------------------------------------------- 1 | instance_meths = %w{instance_variable_get instance_variable_set remove_instance_variable instance_variable_defined?} 2 | complete(:methods=>instance_meths, :class=>"Object#") {|e| e.object.instance_variables } 3 | complete(:method=>"Object#instance_of?", :search=>:modules) { objects_of(Class) } 4 | complete(:methods=>%w{is_a? kind_a? extend}, :class=>"Object#", :search=>:modules) { objects_of(Module) } 5 | complete(:methods=>%w{Object#method Object#respond_to?}) {|e| e.object.methods } 6 | complete(:method=>"Object#[]") {|e| e.object.keys rescue [] } 7 | complete(:method=>"Object#send") {|e| 8 | if e.argument > 1 9 | if (meth = eval(e.arguments[0])) && meth.to_s != 'send' && 10 | (action = MethodMission.find(e.object, meth.to_s)) 11 | e.argument -= 1 12 | e.arguments.shift 13 | action[0].call(e) 14 | end 15 | else 16 | send_methods(e.object) 17 | end 18 | } 19 | def send_methods(obj) 20 | (obj.methods + obj.private_methods(false)).map {|e| e.to_s } - Mission::OPERATORS 21 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'fileutils' 3 | 4 | def gemspec 5 | @gemspec ||= eval(File.read('.gemspec'), binding, '.gemspec') 6 | end 7 | 8 | def gem_file 9 | "#{gemspec.name}-#{gemspec.version}#{ENV['GEM_PLATFORM'] == 'java' ? '-java' : ''}.gem" 10 | end 11 | 12 | desc "Build the gem" 13 | task :gem=>:gemspec do 14 | sh "gem build .gemspec" 15 | FileUtils.mkdir_p 'pkg' 16 | FileUtils.mv gem_file, 'pkg' 17 | end 18 | 19 | desc "Build gems for the default and java platforms" 20 | task :all_gems => :gem do 21 | ENV['GEM_PLATFORM'] = 'java' 22 | @gemspec = nil 23 | Rake::Task["gem"].reenable 24 | Rake::Task["gem"].invoke 25 | end 26 | 27 | desc "Install the gem locally" 28 | task :install => :gem do 29 | sh %{gem install pkg/#{gem_file}} 30 | end 31 | 32 | desc "Generate the gemspec" 33 | task :generate do 34 | puts gemspec.to_ruby 35 | end 36 | 37 | desc "Validate the gemspec" 38 | task :gemspec do 39 | gemspec.validate 40 | end 41 | 42 | desc 'Run tests' 43 | task :test do |t| 44 | sh 'bacon -q -Ilib -I. test/*_test.rb' 45 | end 46 | 47 | task :default => :test 48 | -------------------------------------------------------------------------------- /lib/bond/missions/operator_method_mission.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | # A mission which completes arguments for any module/class method that is an 3 | # operator i.e. '>' or '*'. Takes same Bond.complete options as 4 | # MethodMission. The only operator method this mission doesn't complete is 5 | # '[]='. The operator '[]' should cover the first argument completion of '[]=' 6 | # anyways. 7 | class OperatorMethodMission < MethodMission 8 | OPERATORS = Mission::OPERATORS - ["[]", "[]="] 9 | OBJECTS = Mission::OBJECTS + %w{\S+} 10 | CONDITION = %q{(OBJECTS)\s*(METHODS)\s*(['":])?(.*)$} 11 | 12 | protected 13 | def current_methods 14 | (OPERATORS & MethodMission.action_methods) + ['['] 15 | end 16 | 17 | def matched_method 18 | {'['=>'[]'}[@matched[2]] || @matched[2] 19 | end 20 | 21 | def after_match(input) 22 | set_action_and_search 23 | @completion_prefix, typed = input.to_s.sub(/#{Regexp.quote(@matched[-1])}$/, ''), @matched[-1] 24 | create_input typed, :object => @evaled_object, :argument => 1 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/anywhere_mission_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "anywhere mission" do 4 | before { Bond.agent.reset } 5 | 6 | describe "normally" do 7 | before { complete(:anywhere=>':[^:\s.]*') {|e| %w{:ab :bd :ae} } } 8 | 9 | it "completes at beginning" do 10 | tab(":a").should == %w{:ab :ae} 11 | end 12 | 13 | it "completes in middle of string" do 14 | tab("hash[:a").should == %w{hash[:ab hash[:ae} 15 | end 16 | 17 | it "completes after word break chars" do 18 | tab("{:ab=>1}[:a").should == ["1}[:ab", "1}[:ae"] 19 | tab("nil;:a").should == %w{:ab :ae} 20 | end 21 | end 22 | 23 | it 'with special chars and custom search completes' do 24 | complete(:anywhere=>'\$[^\s.]*', :search=>false) {|e| 25 | global_variables.grep(/^#{Regexp.escape(e.matched[1])}/) 26 | } 27 | tab("$LO").sort.should == ["$LOADED_FEATURES", "$LOAD_PATH"] 28 | end 29 | 30 | it 'with :prefix completes' do 31 | complete(:prefix=>'_', :anywhere=>':[^:\s.]*') { %w{:ab :bd :ae} } 32 | tab("_:a").should == %w{_:ab _:ae} 33 | end 34 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT LICENSE 2 | 3 | Copyright (c) 2010 Gabriel Horner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/m_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "M" do 4 | describe "#load_gems" do 5 | before { $: << '/dir' } 6 | after { $:.pop } 7 | 8 | def mock_file_exists(file) 9 | File.expects(:exist?).at_least(1).returns(false).with {|e| e != file } 10 | File.expects(:exist?).times(1).returns(true).with {|e| e == file } 11 | end 12 | 13 | it "loads gem" do 14 | M.expects(:gem) 15 | mock_file_exists '/dir/boom/../bond' 16 | M.expects(:load_dir).with('/dir/boom/../bond').returns(true) 17 | Bond.load_gems('boom').should == ['boom'] 18 | end 19 | 20 | it "loads plugin gem in gem format" do 21 | M.expects(:find_gem_file).returns(false) 22 | mock_file_exists '/dir/boom/completions/what.rb' 23 | M.expects(:load_file).with('/dir/boom/completions/what.rb') 24 | Bond.load_gems('boom-what').should == ['boom-what'] 25 | end 26 | 27 | it "loads plugin gem in file format" do 28 | M.expects(:find_gem_file).returns(false) 29 | mock_file_exists '/dir/boom/completions/what.rb' 30 | M.expects(:load_file).with('/dir/boom/completions/what.rb') 31 | Bond.load_gems('boom/what.rb').should == ['boom/what.rb'] 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/bond/input.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | # A string representing the last word the user has typed. This string is passed to a mission 3 | # action to generate possible completions. This string contains a number of attributes from the 4 | # matching mission, useful in generating completions. 5 | class Input < String 6 | # Actual object a user has just typed. Used by MethodMission and ObjectMission. 7 | attr_accessor :object 8 | # MatchData object from the matching mission (Mission#matched). 9 | attr_reader :matched 10 | # Current argument number and array of argument strings. Used by MethodMission. 11 | attr_accessor :argument, :arguments 12 | # The full line the user has typed. 13 | attr_reader :line 14 | def initialize(str, options={}) #@private 15 | super(str || '') 16 | @matched = options[:matched] 17 | @line = options[:line] 18 | @object = options[:object] if options[:object] 19 | @argument = options[:argument] if options[:argument] 20 | @arguments = options[:arguments] if options[:arguments] 21 | end 22 | 23 | def inspect #@private 24 | "#" 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/bond/readline.rb: -------------------------------------------------------------------------------- 1 | # This is the default readline plugin for Bond. A valid plugin must be an object 2 | # that responds to methods setup and line_buffer as described below. 3 | class Bond::Readline 4 | DefaultBreakCharacters = " \t\n\"\\'`><=;|&{(" 5 | 6 | # Loads the readline-like library and sets the completion_proc to the given agent. 7 | def self.setup(agent) 8 | readline_setup 9 | 10 | # Reinforcing irb defaults 11 | Readline.completion_append_character = nil 12 | if Readline.respond_to?("basic_word_break_characters=") 13 | Readline.basic_word_break_characters = DefaultBreakCharacters 14 | end 15 | 16 | Readline.completion_proc = agent 17 | end 18 | 19 | def self.readline_setup 20 | require 'readline' 21 | load_extension unless Readline.respond_to?(:line_buffer) 22 | if (Readline::VERSION rescue nil).to_s[/editline/i] 23 | puts "Bond has detected EditLine and may not work with it." + 24 | " See the README's Limitations section." 25 | end 26 | end 27 | 28 | def self.load_extension 29 | require 'readline_line_buffer' 30 | rescue LoadError 31 | $stderr.puts "Bond Error: Failed to load readline_line_buffer. Ensure that it exists and was built correctly." 32 | end 33 | 34 | # Returns full line of what the user has typed. 35 | def self.line_buffer 36 | Readline.line_buffer 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bacon' 2 | require 'bacon/bits' 3 | require 'mocha-on-bacon' 4 | require 'bond' 5 | require 'rbconfig' 6 | 7 | module TestHelpers 8 | extend self 9 | def mock_irb 10 | unless Object.const_defined?(:IRB) 11 | eval %[ 12 | module ::IRB 13 | class< stub(:binding => binding)) 18 | end 19 | end 20 | 21 | def capture_stderr(&block) 22 | original_stderr = $stderr 23 | $stderr = fake = StringIO.new 24 | begin 25 | yield 26 | ensure 27 | $stderr = original_stderr 28 | end 29 | fake.string 30 | end 31 | 32 | def capture_stdout(&block) 33 | original_stdout = $stdout 34 | $stdout = fake = StringIO.new 35 | begin 36 | yield 37 | ensure 38 | $stdout = original_stdout 39 | end 40 | fake.string 41 | end 42 | 43 | def tab(full_line, last_word=full_line) 44 | Bond.agent.weapon.stubs(:line_buffer).returns(full_line) 45 | Bond.agent.call(last_word) 46 | end 47 | 48 | def complete(*args, &block) 49 | Bond.complete(*args, &block) 50 | end 51 | 52 | def valid_readline 53 | Class.new { def self.setup(arg); end; def self.line_buffer; end } 54 | end 55 | 56 | def reset 57 | M.reset 58 | M.debrief :readline => TestHelpers.valid_readline 59 | end 60 | end 61 | 62 | class Bacon::Context 63 | include TestHelpers 64 | end 65 | 66 | # Default settings 67 | Bond::M.debrief(:readline => TestHelpers.valid_readline, :debug => true) 68 | include Bond 69 | -------------------------------------------------------------------------------- /test/mission_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Mission" do 4 | describe "mission" do 5 | before { Bond.agent.reset } 6 | it "completes" do 7 | complete(:on=>/bling/) {|e| %w{ab cd fg hi}} 8 | complete(:method=>'cool') {|e| [] } 9 | tab('some bling f').should == %w{fg} 10 | end 11 | 12 | it "with regexp condition completes" do 13 | complete(:on=>/\s*'([^']+)$/, :search=>false) {|e| %w{coco for puffs}.grep(/#{e.matched[1]}/) } 14 | tab("require 'ff").should == ['puffs'] 15 | end 16 | 17 | it "with non-string completions completes" do 18 | complete(:on=>/.*/) { [:one,:two,:three] } 19 | tab('ok ').should == %w{one two three} 20 | end 21 | 22 | it "with non-array completions completes" do 23 | complete(:on=>/blah/) { 'blah' } 24 | tab('blah ').should == ['blah'] 25 | end 26 | 27 | it "with symbol action completes" do 28 | Rc.module_eval %[def blah(input); %w{one two three}; end] 29 | complete(:on=>/blah/, :action=>:blah) 30 | tab('blah ').should == %w{one two three} 31 | end 32 | 33 | it "with string action completes" do 34 | Rc.module_eval %[def blah(input); %w{one two three}; end] 35 | complete(:on=>/blah/, :action=>'blah') 36 | tab('blah ').should == %w{one two three} 37 | end 38 | 39 | it "always passes Input to action block" do 40 | complete(:on=>/man/) {|e| e.should.be.is_a(Input); [] } 41 | tab('man ') 42 | end 43 | end 44 | 45 | it "sets binding to toplevel binding when not in irb" do 46 | Mission.eval_binding = nil 47 | mock_irb 48 | ::IRB.CurrentContext.expects(:workspace).raises 49 | Mission.eval_binding.should == ::TOPLEVEL_BINDING 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/bond/missions/object_mission.rb: -------------------------------------------------------------------------------- 1 | # A mission which completes an object's methods. For this mission to match, the 2 | # condition must match and the current object must have an ancestor that matches 3 | # :object. Note: To access to the current object being completed on within an 4 | # action, use the input's object attribute. 5 | # 6 | # ==== Bond.complete Options: 7 | # [:action] If an action is not specified, the default action is to complete an 8 | # object's non-operator methods. 9 | # 10 | # ===== Example: 11 | # Bond.complete(:object => 'ActiveRecord::Base') {|input| input.object.class.instance_methods(false) } 12 | class Bond::ObjectMission < Bond::Mission 13 | OBJECTS = %w<\S+> + Bond::Mission::OBJECTS 14 | CONDITION = '(OBJECTS)\.(\w*(?:\?|!)?)$' 15 | def initialize(options={}) #@private 16 | @object_condition = /^#{options[:object]}$/ 17 | options[:on] ||= Regexp.new condition_with_objects 18 | super 19 | end 20 | 21 | def match_message #@private 22 | "Matches completion for object with ancestor matching #{@object_condition.inspect}." 23 | end 24 | 25 | protected 26 | def unique_id 27 | "#{@object_condition.inspect}+#{@on.inspect}" 28 | end 29 | 30 | def do_match(input) 31 | super && eval_object(@matched[1]) && klass(@evaled_object).ancestors.any? {|e| e.to_s =~ @object_condition } 32 | end 33 | 34 | def after_match(input) 35 | @completion_prefix = @matched[1] + "." 36 | @action ||= lambda {|e| default_action(e.object) } 37 | create_input @matched[2], :object => @evaled_object 38 | end 39 | 40 | def default_action(obj) 41 | klass(obj).instance_methods.map {|e| e.to_s} - OPERATORS 42 | end 43 | 44 | def klass(obj) 45 | (class << obj; self; end) 46 | rescue TypeError # can't define singleton 47 | obj.class 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/bond/rc.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | # Namespace in which completion files, ~/.bondrc and ~/.bond/completions/*.rb, are evaluated. Methods in this module 3 | # and Search are the DSL in completion files and can be used within completion actions. 4 | # 5 | # === Example ~/.bondrc 6 | # # complete arguments for any object's :respond_to? 7 | # complete(:method => "Object#respond_to?") {|e| e.object.methods } 8 | # # complete arguments for any module's :public 9 | # complete(:method => "Module#public") {|e| e.object.instance_methods } 10 | # 11 | # # Share generate_tags action across completions 12 | # complete(:method => "edit_tags", :action => :generate_tags) 13 | # complete(:method => "delete_tags", :search => false) {|e| generate_tags(e).grep(/#{e}/i) } 14 | # 15 | # def generate_tags(input) 16 | # ... 17 | # end 18 | module Rc 19 | extend self, Search 20 | 21 | # See {Bond#complete} 22 | def complete(*args, &block); M.complete(*args, &block); end 23 | # See {Bond#recomplete} 24 | def recomplete(*args, &block); M.recomplete(*args, &block); end 25 | 26 | # Action method with search which returns array of files that match current input. 27 | def files(input) 28 | (::Readline::FILENAME_COMPLETION_PROC.call(input) || []).map {|f| 29 | f =~ /^~/ ? File.expand_path(f) : f 30 | } 31 | end 32 | 33 | # Helper method which returns objects of a given class. 34 | def objects_of(klass) 35 | object = [] 36 | ObjectSpace.each_object(klass) {|e| object.push(e) } 37 | object 38 | end 39 | 40 | # Calls eval with Mission.current_eval, rescuing any exceptions to return nil. 41 | # If Bond.config[:debug] is true, exceptions are raised again. 42 | def eval(str) 43 | Mission.current_eval(str) 44 | rescue Exception 45 | raise if Bond.config[:debug] 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/object_mission_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "ObjectMission" do 4 | before { Bond.agent.reset } 5 | describe "object mission" do 6 | it "with default action completes" do 7 | complete(:object=>"String") 8 | complete(:on=>/man/) { %w{upper upster upful}} 9 | tab("'man'.up").sort.should == [".upcase", ".upcase!", ".upto"] 10 | end 11 | 12 | it "with regex condition completes" do 13 | complete(:object=>'Str.*') {|e| e.object.class.superclass.instance_methods(true) } 14 | complete(:on=>/man/) { %w{upper upster upful}} 15 | tab("'man'.unta").should == [".untaint"] 16 | end 17 | 18 | it "with explicit action completes" do 19 | complete(:object=>"String") {|e| e.object.class.superclass.instance_methods(true) } 20 | complete(:on=>/man/) { %w{upper upster upful}} 21 | tab("'man'.unta").should == [".untaint"] 22 | end 23 | 24 | it "completes without including word break characters" do 25 | complete(:object=>"Hash") 26 | matches = tab("{}.f") 27 | matches.size.should.be > 0 28 | matches.all? {|e| !e.include?('{')}.should == true 29 | end 30 | 31 | it "completes with additional text after completion point" do 32 | complete(:object=>"Object") 33 | tab(':man.f blah', ':man.f').include?(':man.freeze').should == true 34 | end 35 | 36 | it "doesn't evaluate anything before the completion object" do 37 | complete(:object=>'Object') 38 | tab('raise :man.i').size.should > 0 39 | end 40 | 41 | it "ignores invalid ruby" do 42 | complete(:object=>"String") 43 | tab("blah.upt").should == [] 44 | end 45 | 46 | # needed to ensure Bond works in irbrc 47 | it "doesn't evaluate irb binding on definition" do 48 | Object.expects(:const_defined?).never 49 | complete(:object=>"String") 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'rubygems' unless Object.const_defined?(:Gem) 3 | require File.dirname(__FILE__) + "/lib/bond/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "bond" 7 | s.version = Bond::VERSION 8 | s.platform = ENV['GEM_PLATFORM'] if ENV['GEM_PLATFORM'] 9 | s.authors = ["Gabriel Horner"] 10 | s.email = "gabriel.horner@gmail.com" 11 | s.homepage = "http://tagaholic.me/bond/" 12 | s.summary = "Mission: Easy custom autocompletion for arguments, methods and beyond. Accomplished for irb and any other readline-like console environments." 13 | s.description = "Bond is on a mission to improve autocompletion in ruby, especially for irb/ripl. Aside from doing everything irb's can do and fixing its quirks, Bond can autocomplete argument(s) to methods, uniquely completing per module, per method and per argument. Bond brings ruby autocompletion closer to bash/zsh as it provides a configuration system and a DSL for creating custom completions and completion rules. With this configuration system, users can customize their autocompletions and share it with others. Bond can also load completions that ship with gems. Bond is able to offer more than irb's completion since it uses the full line of input when completing as opposed to irb's last-word approach." 14 | s.required_rubygems_version = ">= 1.3.6" 15 | s.has_rdoc = 'yard' 16 | s.rdoc_options = ['--title', "Bond #{Bond::VERSION} Documentation"] 17 | s.add_development_dependency 'bacon', '>= 1.1.0' 18 | s.add_development_dependency 'mocha', '~> 0.12.1' 19 | s.add_development_dependency 'mocha-on-bacon', '~> 0.2.1' 20 | s.add_development_dependency 'bacon-bits' 21 | s.files = Dir.glob(%w[{lib,test}/**/*.rb bin/* [A-Z]*.{txt,rdoc,md}]) + %w{Rakefile .gemspec .travis.yml} 22 | if ENV['GEM_PLATFORM'] != 'java' 23 | s.files += Dir.glob("ext/**/*.{rb,c}") 24 | s.extensions = ["ext/readline_line_buffer/extconf.rb"] 25 | end 26 | s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"] 27 | s.license = 'MIT' 28 | end 29 | -------------------------------------------------------------------------------- /test/operator_method_mission_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "operator method mission" do 4 | before_all { MethodMission.reset } 5 | before { Bond.agent.reset; Bond.complete(:all_operator_methods=>true) } 6 | 7 | describe "operator" do 8 | before { complete(:method=>"Hash#[]") { %w{ab cd ae} } } 9 | 10 | it "completes" do 11 | tab('{}[a').should == ["}[ab", "}[ae"] 12 | end 13 | 14 | it "completes quoted argument" do 15 | tab('{:a=>1}["a').should == %w{ab ae} 16 | end 17 | 18 | it "completes symbolic argument" do 19 | tab('{}[:a').should == ["}[:ab", "}[:ae"] 20 | end 21 | 22 | it "completes with no space between method and argument" do 23 | tab('{}[a').should == ["}[ab", "}[ae"] 24 | end 25 | 26 | it "completes with space between method and argument" do 27 | tab('{}[ a').should == ["ab", "ae"] 28 | end 29 | 30 | it "completes with operator characters in object" do 31 | tab('{:a=> 1}[').should == ["1}[ab", "1}[cd", "1}[ae"] 32 | end 33 | 34 | it "completes all arguments with only space as argument" do 35 | tab('{}[ ').should == ["ab", "cd", "ae"] 36 | end 37 | 38 | it "completes with a chain of objects" do 39 | tab('Hash.new[a').should == %w{Hash.new[ab Hash.new[ae} 40 | end 41 | 42 | it "completes in middle of line" do 43 | tab('nil; {}[a').should == ["}[ab", "}[ae"] 44 | end 45 | 46 | it "doesn't complete for multiple arguments" do 47 | tab('{}[a,').should == [] 48 | end 49 | end 50 | 51 | it "operator with space between object and method completes" do 52 | complete(:method=>"Array#*") { %w{ab cd ae} } 53 | tab('[1,2] * a').should == %w{ab ae} 54 | tab('[1,2] *a').should == %w{*ab *ae} 55 | end 56 | 57 | it "class operator completes" do 58 | complete(:method=>"Hash.*") { %w{ab cd ae} } 59 | tab('Hash * a').should == %w{ab ae} 60 | end 61 | 62 | it "with :search completes" do 63 | complete(:method=>"Array#*", :search=>:anywhere) { %w{abc bcd cde} } 64 | tab('[1, 2] * b').should == ['abc', 'bcd'] 65 | end 66 | end -------------------------------------------------------------------------------- /test/completions_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "completions for" do 4 | before_all { 5 | reset 6 | complete(:all_methods=>true) 7 | complete(:all_operator_methods=>true) 8 | M.load_file File.dirname(__FILE__) + '/../lib/bond/completion.rb' 9 | M.load_dir File.dirname(__FILE__) + '/../lib/bond' 10 | } 11 | 12 | it "Array#delete" do 13 | tab("[12,23,34,15].delete 1").should == %w{12 15} 14 | end 15 | 16 | describe "Hash" do 17 | before { @hash = %q{{:ab=>1,:bc=>1,:cd=>3,:ae=>2}} } 18 | 19 | it "#delete" do 20 | tab("#{@hash}.delete :a").sort.should == %w{:ab :ae} 21 | end 22 | 23 | it "#index" do 24 | tab("#{@hash}.index 2").should == %w{2} 25 | end 26 | 27 | it "#[]" do 28 | tab("#{@hash}['a").sort.should == %w{ab ae} 29 | end 30 | end 31 | 32 | describe "Kernel" do 33 | it "#raise" do 34 | tab("raise Errno::ETIME").sort.should == %w{Errno::ETIME Errno::ETIMEDOUT} 35 | end 36 | 37 | it "#require" do 38 | mock_libs = ['net/http.rb', 'net/http/get.rb', 'abbrev.rb'].map {|e| $:[0] + "/#{e}" } 39 | Dir.stubs(:[]).returns(mock_libs) 40 | tab("require 'net/htt").should == %w{net/http.rb net/http/} 41 | end 42 | end 43 | 44 | describe "Object" do 45 | it "#instance_of?" do 46 | expectations = ['Hash'] 47 | expectations = ["Hash", "Hash::"] if RbConfig::CONFIG["RUBY_SO_NAME"].to_s[/rubinius/i] 48 | tab("[].instance_of? Has").should == expectations 49 | end 50 | 51 | it "#is_a?" do 52 | tab("Module.is_a? Mod").should == ['Module'] 53 | end 54 | 55 | it "#send" do 56 | tab("Object.send :ne").should == [':new'] 57 | end 58 | 59 | it "#send and additional arguments" do 60 | tab('Bond.send :const_get, Ag').should == ['Agent'] 61 | end 62 | 63 | it "#send and invalid first argument" do 64 | tab('Bond.send :blah, ').should == [] 65 | end 66 | 67 | it "#instance_variable_get" do 68 | tab("Bond::M.instance_variable_get '@a").should == ['@agent'] 69 | end 70 | 71 | it "#method" do 72 | tab("Bond::M.method :ho").should == [':home'] 73 | end 74 | 75 | it "#[]" do 76 | ::ENV['ZZZ'] = ::ENV['ZZY'] = 'blah' 77 | tab("ENV['ZZ").should == %w{ZZY ZZZ} 78 | end 79 | end 80 | 81 | describe "Module" do 82 | it "#const_get" do 83 | tab("Bond.const_get M").sort.should == ['M', 'MethodMission', 'Mission'] 84 | end 85 | 86 | it "#instance_methods" do 87 | tab("Bond::Agent.instance_method :ca").should == [':call'] 88 | end 89 | 90 | it "#>" do 91 | tab("Object > Mod").should == %w{Module} 92 | end 93 | 94 | it "#> and :files search" do 95 | tab("Object > Bon").should == %w{Bond Bond::} 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/bond/search.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | # Contains search methods used to filter possible completions given what the user has typed for that completion. 3 | # For a search method to be used by Bond.complete it must end in '_search' and take two arguments: the Input 4 | # string and an array of possible completions. 5 | # 6 | # ==== Creating a search method 7 | # Say you want to create a custom search which ignores completions containing '-'. 8 | # In a completion file under Rc namespace, define this method: 9 | # def ignore_hyphen_search(input, list) 10 | # normal_search(input, list.select {|e| e !~ /-/ }) 11 | # end 12 | # 13 | # Now you can pass this custom search to any complete() as :search => :ignore_hyphen 14 | module Search 15 | class< e 48 | completion_error(e.message, "Completion Info: #{e.mission.match_message}") 49 | rescue 50 | completion_error "Failed internally with '#{$!.message}'.", 51 | "Please report this issue with debug on: Bond.config[:debug] = true." 52 | end 53 | 54 | # Given a hypothetical user input, reports back what mission it would have 55 | # found and executed. 56 | def spy(input) 57 | if (mission = find_mission(input)) 58 | puts mission.match_message, "Possible completions: #{mission.execute.inspect}", 59 | "Matches for #{mission.condition.inspect} are #{mission.matched.to_a.inspect}" 60 | else 61 | puts "Doesn't match a completion." 62 | end 63 | rescue FailedMissionError => e 64 | puts e.mission.match_message, e.message, 65 | "Matches for #{e.mission.condition.inspect} are #{e.mission.matched.to_a.inspect}" 66 | end 67 | 68 | def find_mission(input) #@private 69 | @missions.find {|mission| mission.matches?(input) } 70 | end 71 | 72 | # Default mission used by agent. An instance of DefaultMission. 73 | def default_mission 74 | @default_mission ||= DefaultMission.new(:action => @default_mission_action) 75 | end 76 | 77 | # Resets an agent's missions 78 | def reset 79 | @missions = [] 80 | end 81 | 82 | protected 83 | def setup_readline(plugin) 84 | @weapon = plugin 85 | @weapon.setup(self) 86 | rescue 87 | $stderr.puts "Bond Error: Failed #{plugin.to_s[/[^:]+$/]} setup with '#{$!.message}'" 88 | end 89 | 90 | def create_mission(options, &block) 91 | Mission.create options.merge!(:action => options[:action] || block) 92 | rescue InvalidMissionError 93 | "Invalid #{$!.message} for completion with options: #{options.inspect}" 94 | rescue 95 | "Unexpected error while creating completion with options #{options.inspect} and message:\n#{$!}" 96 | end 97 | 98 | def sort_last_missions 99 | @missions.replace @missions.partition {|e| e.place != :last }.flatten 100 | end 101 | 102 | def completion_error(desc, message) 103 | arr = ["Bond Error: #{desc}", message] 104 | arr << "Stack Trace: #{$!.backtrace.inspect}" if Bond.config[:debug] 105 | arr 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/bond_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Bond" do 4 | describe "start" do 5 | def start(options={}, &block) 6 | Bond.start({:readline=>valid_readline}.merge(options), &block) 7 | end 8 | 9 | before { M.instance_eval("@started = @agent = @config = nil"); M.expects(:load_completions) } 10 | it "prints error if readline doesn't have all required methods" do 11 | capture_stderr { 12 | start :readline=>Module.new{ def self.setup(arg); end } 13 | }.should =~ /Invalid/ 14 | end 15 | 16 | it "prints error if readline symbol is invalid" do 17 | capture_stderr { 18 | start :readline => :blah 19 | }.should =~ /Invalid.*'blah'/ 20 | end 21 | 22 | it "prints no error if valid readline" do 23 | capture_stderr { start }.should == '' 24 | end 25 | 26 | it 'prints no error if valid readline symbol' do 27 | capture_stderr { start :readline => :ruby }.should == '' 28 | Bond.config[:readline].should == Bond::Ruby 29 | end 30 | 31 | it "sets default mission" do 32 | start :default_mission=>lambda {|e| %w{1 2 3}} 33 | tab('1').should == ['1'] 34 | end 35 | 36 | it "sets default search" do 37 | start :default_search=>:anywhere 38 | complete(:on=>/blah/) { %w{all_quiet on_the western_front}} 39 | tab('blah qu').should == ["all_quiet"] 40 | end 41 | 42 | it "defines completion in block" do 43 | start { complete(:on=>/blah/) { %w{all_quiet on_the western_front}} } 44 | tab('blah all').should == ["all_quiet"] 45 | end 46 | 47 | it "sets proc eval_binding" do 48 | bdg = binding 49 | start :eval_binding => lambda { bdg } 50 | Mission.expects(:eval).with(anything, bdg).returns([]) 51 | tab("'blah'.").should == [] 52 | end 53 | 54 | it "status can be checked with started?" do 55 | Bond.started?.should == false 56 | start 57 | Bond.started?.should == true 58 | end 59 | 60 | after_all { reset } 61 | end 62 | 63 | describe "start with :gems" do 64 | before { 65 | File.stubs(:exist?).returns(true) 66 | M.stubs(:load_file) 67 | } 68 | 69 | it "attempts to load gem" do 70 | M.stubs(:load_dir) 71 | M.expects(:gem).twice 72 | start(:gems=>%w{one two}) 73 | end 74 | 75 | it "rescues nonexistent gem" do 76 | M.stubs(:load_dir) 77 | M.expects(:gem).raises(LoadError) 78 | should.not.raise { start(:gems=>%w{blah}) } 79 | end 80 | 81 | it "rescues nonexistent method 'gem'" do 82 | M.stubs(:load_dir) 83 | M.expects(:gem).raises(NoMethodError) 84 | should.not.raise { start(:gems=>%w{blah}) } 85 | end 86 | 87 | it "prints error if gem completion not found" do 88 | M.stubs(:load_dir) 89 | M.expects(:find_gem_file).returns(nil) 90 | capture_stderr { start(:gems=>%w{invalid}) }.should =~ /No completions.*'invalid'/ 91 | end 92 | 93 | it "loads gem completion file" do 94 | M.expects(:load_dir) 95 | M.expects(:load_dir).with(File.join($:[0], 'awesome', '..', 'bond')) 96 | M.expects(:load_dir) 97 | M.expects(:gem) 98 | start(:gems=>%w{awesome}) 99 | end 100 | after_all { reset } 101 | end 102 | 103 | it "prints error if Readline setup fails" do 104 | Bond::Readline.expects(:setup).raises('WTF') 105 | capture_stderr { Bond.start(:readline=>Bond::Readline) }.should =~ /Error.*Failed Readline.*'WTF'/ 106 | M.debrief :readline=>valid_readline 107 | end 108 | 109 | it "start prints error for failed completion file" do 110 | Rc.stubs(:module_eval).raises('wtf') 111 | capture_stderr { Bond.start }.should =~ /Bond Error: Completion file.*with:\nwtf/ 112 | end 113 | 114 | it "reset clears existing missions" do 115 | complete(:on=>/blah/) {[]} 116 | Bond.agent.missions.size.should.not == 0 117 | reset 118 | Bond.agent.missions.size.should == 0 119 | end 120 | 121 | describe "restart" do 122 | def start(options={}, &block) 123 | Bond.start({:readline=>valid_readline}.merge(options), &block) 124 | end 125 | 126 | it "deletes previous config" do 127 | start :blah=>'' 128 | Bond.config[:blah].should.not == nil 129 | Bond.restart({:readline=>valid_readline}) 130 | Bond.config[:blah].should == nil 131 | end 132 | 133 | it "deletes previous method completions" do 134 | start 135 | complete(:method=>'blah') { [] } 136 | MethodMission.actions['blah'].should.not == nil 137 | Bond.restart({:readline=>valid_readline}) 138 | MethodMission.actions['blah'].should == nil 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/bond/m.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | # Takes international quagmires (a user's completion setup) and passes them on 3 | # as missions to an Agent. 4 | module M 5 | extend self 6 | 7 | # See {Bond#complete} 8 | def complete(options={}, &block) 9 | if (result = agent.complete(options, &block)).is_a?(String) 10 | $stderr.puts "Bond Error: "+result 11 | false 12 | else 13 | true 14 | end 15 | end 16 | 17 | # See {Bond#recomplete} 18 | def recomplete(options={}, &block) 19 | if (result = agent.recomplete(options, &block)).is_a?(String) 20 | $stderr.puts "Bond Error: "+result 21 | false 22 | else 23 | true 24 | end 25 | end 26 | 27 | # See {Bond#agent} 28 | def agent 29 | @agent ||= Agent.new(config) 30 | end 31 | 32 | # See {Bond#config} 33 | def config 34 | @config ||= {:debug => false, :default_search => :underscore} 35 | end 36 | 37 | # Resets M's missions and config 38 | def reset 39 | MethodMission.reset 40 | @config = @agent = nil 41 | end 42 | 43 | # See {Bond#spy} 44 | def spy(input) 45 | agent.spy(input) 46 | end 47 | 48 | # Validates and sets values in M.config. 49 | def debrief(options={}) 50 | config.merge! options 51 | config[:readline] ||= default_readline 52 | if !config[:readline].is_a?(Module) && 53 | Bond.const_defined?(config[:readline].to_s.capitalize) 54 | config[:readline] = Bond.const_get(config[:readline].to_s.capitalize) 55 | end 56 | unless %w{setup line_buffer}.all? {|e| config[:readline].respond_to?(e) } 57 | $stderr.puts "Bond Error: Invalid readline plugin '#{config[:readline]}'." 58 | end 59 | end 60 | 61 | # See {Bond#restart} 62 | def restart(options={}, &block) 63 | reset 64 | start(options, &block) 65 | end 66 | 67 | # See {Bond#start} 68 | def start(options={}, &block) 69 | debrief options 70 | @started = true 71 | load_completions 72 | Rc.module_eval(&block) if block 73 | true 74 | end 75 | 76 | # See {Bond#started?} 77 | def started? 78 | !!@started 79 | end 80 | 81 | # Finds the full path to a gem's file relative it's load path directory. 82 | # Returns nil if not found. 83 | def find_gem_file(rubygem, file) 84 | begin gem(rubygem); rescue Exception; end 85 | (dir = $:.find {|e| File.exist?(File.join(e, file)) }) && File.join(dir, file) 86 | end 87 | 88 | # Loads a completion file in Rc namespace. 89 | def load_file(file) 90 | Rc.module_eval File.read(file) 91 | rescue Exception => e 92 | $stderr.puts "Bond Error: Completion file '#{file}' failed to load with:", e.message 93 | end 94 | 95 | # Loads completion files in given directory. 96 | def load_dir(base_dir) 97 | if File.exist?(dir = File.join(base_dir, 'completions')) 98 | Dir[dir + '/*.rb'].each {|file| load_file(file) } 99 | true 100 | end 101 | end 102 | 103 | # Loads completions from gems 104 | def load_gems(*gems) 105 | gems.select {|e| load_gem_completion(e) } 106 | end 107 | 108 | # Find a user's home in a cross-platform way 109 | def home 110 | ['HOME', 'USERPROFILE'].each {|e| return ENV[e] if ENV[e] } 111 | return "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] 112 | File.expand_path("~") 113 | rescue 114 | File::ALT_SEPARATOR ? "C:/" : "/" 115 | end 116 | 117 | protected 118 | def default_readline 119 | RUBY_PLATFORM[/mswin|mingw|bccwin|wince/i] ? Ruby : 120 | RUBY_PLATFORM[/java/i] ? Jruby : Bond::Readline 121 | end 122 | 123 | def load_gem_completion(rubygem) 124 | (dir = find_gem_file(rubygem, File.join(rubygem, '..', 'bond'))) ? load_dir(dir) : 125 | rubygem[/\/|-/] ? load_plugin_file(rubygem) : 126 | $stderr.puts("Bond Error: No completions found for gem '#{rubygem}'.") 127 | end 128 | 129 | def load_plugin_file(rubygem) 130 | namespace, file = rubygem.split(/\/|-/, 2) 131 | file += '.rb' unless file[/\.rb$/] 132 | if (dir = $:.find {|e| File.exist?(File.join(e, namespace, 'completions', file)) }) 133 | load_file File.join(dir, namespace, 'completions', file) 134 | true 135 | end 136 | end 137 | 138 | def load_completions 139 | load_file File.join(File.dirname(__FILE__), 'completion.rb') unless config[:bare] 140 | load_dir File.dirname(__FILE__) unless config[:bare] 141 | load_gems *config[:gems] if config[:gems] 142 | load_file(File.join(home,'.bondrc')) if File.exist?(File.join(home, '.bondrc')) && !config[:bare] 143 | load_dir File.join(home, '.bond') unless config[:bare] 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /test/search_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Search" do 4 | before { Bond.agent.reset } 5 | 6 | describe "mission with search" do 7 | it "false completes" do 8 | complete(:on=>/cool '(.*)/, :search=>false) {|e| %w{coco for puffs}.grep(/#{e.matched[1]}/) } 9 | tab("cool 'ff").should == ['puffs'] 10 | end 11 | 12 | it "defined in Rc completes" do 13 | Rc.module_eval %q{def coco_search(input, list); list.grep(/#{input}/); end } 14 | complete(:on=>/blah/, :search=>:coco) {|e| %w{coco for puffs} } 15 | tab("blah ff").should == ['puffs'] 16 | end 17 | 18 | it ":anywhere completes" do 19 | complete(:on=>/blah/, :search=>:anywhere) {|e| %w{coco for puffs} } 20 | tab("blah ff").should == ['puffs'] 21 | end 22 | 23 | it ":ignore_case completes" do 24 | complete(:on=>/blah/, :search=>:ignore_case) {|e| %w{Coco For PufFs} } 25 | tab("blah pu").should == ['PufFs'] 26 | end 27 | 28 | it ":underscore completes" do 29 | complete(:on=>/blah/, :search=>:underscore) {|e| %w{and_one big_two can_three} } 30 | tab("blah and").should == ['and_one'] 31 | tab("blah b_t").should == ['big_two'] 32 | end 33 | end 34 | 35 | it "underscore search doesn't pick up strings starting with __" do 36 | completions = ["include?", "instance_variable_defined?", "__id__", "include_and_exclude?"] 37 | complete(:on=>/blah/, :search=>:underscore) { completions } 38 | tab("blah i").should == ["include?", "instance_variable_defined?", "include_and_exclude?"] 39 | end 40 | 41 | it "underscore search autocompletes strings starting with __" do 42 | completions = ["include?", "__id__", "__send__"] 43 | complete(:on=>/blah/, :search=>:underscore) { completions } 44 | tab('blah _').should == ["__id__", "__send__"] 45 | tab('blah __').should == ["__id__", "__send__"] 46 | tab('blah __i').should == ["__id__"] 47 | end 48 | 49 | it "underscore search can match first unique strings of each underscored word" do 50 | completions = %w{so_long so_larger so_louder} 51 | complete(:on=>/blah/, :search=>:underscore) { completions } 52 | tab("blah s_lo").should == %w{so_long so_louder} 53 | tab("blah s_lou").should == %w{so_louder} 54 | end 55 | 56 | it "underscore search acts normal if ending in underscore" do 57 | complete(:on=>/blah/, :search=>:underscore) {|e| %w{and_one big_two can_three ander_one} } 58 | tab("blah and_").should == %w{and_one} 59 | end 60 | 61 | it "search handles completions with regex characters" do 62 | completions = ['[doh]', '.*a', '?ok'] 63 | complete(:on=>/blah/) { completions } 64 | tab('blah .').should == ['.*a'] 65 | tab('blah [').should == ['[doh]'] 66 | tab('blah ?').should == ['?ok'] 67 | end 68 | 69 | it "default search uses default search" do 70 | Search.default_search.should == :underscore 71 | Rc.expects(:underscore_search).with('a', %w{ab cd}) 72 | Rc.send(:default_search, 'a', %w{ab cd}) 73 | end 74 | 75 | describe "modules search" do 76 | before { 77 | complete(:on=>/blah/, :search=>:modules) { %w{A1 M1::Z M1::Y::X M2::X} } 78 | } 79 | it "completes all modules" do 80 | tab('blah ').should == ["A1", "M1::", "M2::"] 81 | end 82 | 83 | it "completes single first level module" do 84 | tab('blah A').should == %w{A1} 85 | end 86 | 87 | it "completes single first level module parent" do 88 | tab('blah M2').should == %w{M2::} 89 | end 90 | 91 | it "completes all second level modules" do 92 | tab('blah M1::').should == %w{M1::Z M1::Y::} 93 | end 94 | 95 | it "completes second level module parent" do 96 | tab('blah M1::Y').should == %w{M1::Y::} 97 | end 98 | 99 | it "completes third level module" do 100 | tab('blah M1::Y::').should == %w{M1::Y::X} 101 | end 102 | end 103 | 104 | describe "files search" do 105 | before { 106 | complete(:on=>/rm/, :search=>:files) { %w{a1 d1/f2 d1/d2/f1 d2/f1 d2/f1/ /f1} } 107 | } 108 | it "completes all paths" do 109 | tab('rm ').should == %w{a1 d1/ d2/ /} 110 | end 111 | 112 | it "completes single first level file" do 113 | tab('rm a').should == %w{a1} 114 | end 115 | 116 | it "completes single first level directory" do 117 | tab('rm d2').should == %w{d2/} 118 | end 119 | 120 | it "completes all second level paths" do 121 | tab('rm d1/').should == %w{d1/f2 d1/d2/} 122 | end 123 | 124 | it "completes single second level directory" do 125 | tab('rm d1/d2').should == %w{d1/d2/} 126 | end 127 | 128 | it "completes single third level file" do 129 | tab('rm d1/d2/').should == %w{d1/d2/f1} 130 | end 131 | 132 | it "completes file and directory with same name" do 133 | tab('rm d2/f').should == %w{d2/f1 d2/f1/} 134 | end 135 | 136 | it "completes file with full path" do 137 | tab('rm /f').should == %w{/f1} 138 | end 139 | end 140 | end -------------------------------------------------------------------------------- /test/completion_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Completion" do 4 | before_all { 5 | reset 6 | M.load_file File.dirname(__FILE__) + '/../lib/bond/completion.rb' 7 | M.load_dir File.dirname(__FILE__) + '/../lib/bond' 8 | } 9 | 10 | it "completes global variables anywhere" do 11 | tab("blah $LOA").should.satisfy {|e| 12 | e.size > 0 && e.all? {|e| e=~ /^\$LOA/} } 13 | tab("h[$LOADED").should == ["h[$LOADED_FEATURES"] 14 | end 15 | 16 | it "completes absolute constants anywhere" do 17 | tab("blah ::Has").should == ["::Hash"] 18 | tab("h[::Has").should == ["h[::Hash"] 19 | end 20 | 21 | it "completes invalid constants safely" do 22 | capture_stdout { 23 | tab("Bond::MethodMission ").should == [] 24 | }.should == '' 25 | end 26 | 27 | it "completes nested classes greater than 2 levels" do 28 | eval %[module ::Foo; module Bar; module Baz; end; end; end] 29 | tab("Foo::Bar::B").should == %w{Foo::Bar::Baz} 30 | end 31 | 32 | it "completes nested classes anywhere" do 33 | tab("module Blah; include Bond::Sea").should == ["Bond::Search"] 34 | end 35 | 36 | it "completes symbols anywhere" do 37 | Symbol.expects(:all_symbols).twice.returns([:mah]) 38 | tab("blah :m").size.should.be > 0 39 | tab("blah[:m").should == ["blah[:mah"] 40 | end 41 | 42 | it "completes method arguments with parenthesis" do 43 | tab("%w{ab bc cd}.delete(").should == %w{ab bc cd} 44 | end 45 | 46 | it "completes method arguments when object contains method names" do 47 | tab("%w{find ab cd}.delete ").should == %w{find ab cd} 48 | end 49 | 50 | it "completes hash coming from a method" do 51 | tab('Bond.config[:r').should == ["Bond.config[:readline"] 52 | end 53 | 54 | it "methods don't swallow up default completion" do 55 | Bond.agent.find_mission("some_method(:verbose=>true) { Arr").should == nil 56 | end 57 | 58 | describe "completes object methods" do 59 | def be_methods_from(klass, regex, obj=klass.new) 60 | lambda {|e| 61 | meths = e.map {|f| f.sub(/^#{Regexp.quote(regex)}/, '') } 62 | meths.size.should.be > 0 63 | (meths - obj.methods.map {|e| e.to_s} - Mission::OPERATORS).size.should == 0 64 | } 65 | end 66 | 67 | shared "objects" do 68 | it "non whitespace object" do 69 | tab(':man.').should be_methods_from(Symbol, ':man.', :man) 70 | end 71 | 72 | it "nil" do 73 | tab("nil.t").should be_methods_from(NilClass, 'nil.', nil) 74 | end 75 | 76 | it "false" do 77 | tab("false.f").should be_methods_from(FalseClass, 'false.', false) 78 | end 79 | 80 | it "strings" do 81 | tab("'man oh'.s").should be_methods_from(String, '.') 82 | tab('"man oh".s').should be_methods_from(String, '.') 83 | end 84 | 85 | it "array" do 86 | tab("[1, 2].f").should be_methods_from(Array, '2].') 87 | end 88 | 89 | it "hash" do 90 | tab("{:a =>1}.f").should be_methods_from(Hash, '1}.') 91 | end 92 | 93 | it "regexp" do 94 | tab("/man oh/.c").should be_methods_from(Regexp, 'oh/.', /man oh/) 95 | end 96 | 97 | it "proc" do 98 | tab('lambda { }.c').should be_methods_from(Proc, '}.', lambda{}) 99 | tab('proc { }.c').should be_methods_from(Proc, '}.', lambda{}) 100 | end 101 | 102 | it "range" do 103 | tab("(1 .. 10).m").should be_methods_from(Range, '10).', (1..10)) 104 | end 105 | 106 | it "object between ()" do 107 | tab("(2 * 2).").should be_methods_from(Fixnum, '2).', 2) 108 | tab("String.new('man oh').s").should be_methods_from(String, ').') 109 | end 110 | 111 | it "object quoted by {}" do 112 | tab("%r{man oh}.c").should be_methods_from(Regexp, 'oh}.', /man oh/) 113 | tab("%q{man oh}.s").should be_methods_from(String, 'oh}.') 114 | tab("%Q{man oh}.s").should be_methods_from(String, 'oh}.') 115 | tab("%w{man oh}.f").should be_methods_from(Array, 'oh}.') 116 | tab("%s{man oh}.t").should be_methods_from(Symbol, 'oh}.', :man) 117 | tab("%{man oh}.t").should be_methods_from(String, 'oh}.') 118 | end 119 | 120 | it "object quoted by []" do 121 | tab("%r[man oh].c").should be_methods_from(Regexp, 'oh].', /man oh/) 122 | tab("%q[man oh].s").should be_methods_from(String, 'oh].') 123 | tab("%Q[man oh].s").should be_methods_from(String, 'oh].') 124 | tab("%w[man oh].f").should be_methods_from(Array, 'oh].') 125 | tab("%s[man oh].t").should be_methods_from(Symbol, 'oh].', :man) 126 | tab("%[man oh].t").should be_methods_from(String, 'oh].') 127 | end 128 | 129 | it "with overridden class method" do 130 | complete :on=>/(.*)./, :object=>'Object' 131 | tab("obj = Object.new; def obj.class; end; obj.").should be_methods_from(Object, 'obj.') 132 | end 133 | end 134 | 135 | describe "for" do 136 | behaves_like "objects" 137 | end 138 | 139 | describe "after valid ruby for" do 140 | def tab(str) 141 | super("nil; "+str) 142 | end 143 | behaves_like "objects" 144 | end 145 | 146 | describe "after invalid ruby for" do 147 | def tab(str) 148 | super("blah "+str) 149 | end 150 | behaves_like "objects" 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/bond/mission.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | # Occurs when a mission is incorrectly defined. 3 | class InvalidMissionError < StandardError; end 4 | # Occurs when a mission fails. 5 | class FailedMissionError < StandardError 6 | # Mission that failed 7 | attr_reader :mission 8 | def initialize(mission); @mission = mission; end #@private 9 | end 10 | 11 | # Represents a completion rule, given a condition (:on) on which to match and an action 12 | # (block or :action) with which to generate possible completions. 13 | class Mission 14 | class< == === =~ > >= >> [] []= ^ | ~ ! != !~} 41 | # Regular expressions which describe common objects for MethodMission and ObjectMission 42 | OBJECTS = %w<\([^\)]*\) '[^']*' "[^"]*" \/[^\/]*\/> + 43 | %w<(?:%q|%r|%Q|%w|%s|%)?\[[^\]]*\] (?:proc|lambda|%q|%r|%Q|%w|%s|%)?\s*\{[^\}]*\}> 44 | 45 | # Generates array of possible completions and searches them if search is disabled. Any values 46 | # that aren't strings are automatically converted with to_s. 47 | attr_reader :action 48 | # See {Bond#complete}'s :place. 49 | attr_reader :place 50 | # A MatchData object generated from matching the user input with the condition. 51 | attr_reader :matched 52 | # Regexp condition 53 | attr_reader :on 54 | # Takes same options as {Bond#complete}. 55 | def initialize(options) 56 | raise InvalidMissionError, ":action" unless (options[:action] || respond_to?(:default_action, true)) 57 | raise InvalidMissionError, ":on" unless (options[:on] && options[:on].is_a?(Regexp)) || respond_to?(:default_on, true) 58 | @action, @on = options[:action], options[:on] 59 | @place = options[:place] if options[:place] 60 | @name = options[:name] if options[:name] 61 | @search = options.has_key?(:search) ? options[:search] : Search.default_search 62 | end 63 | 64 | # Returns a boolean indicating if a mission matches the given Input and should be executed for completion. 65 | def matches?(input) 66 | @matched = @input = @completion_prefix = nil 67 | (match = do_match(input)) && after_match(@line = input) 68 | !!match 69 | end 70 | 71 | # Called when a mission has been chosen to autocomplete. 72 | def execute(input=@input) 73 | completions = Array(call_action(input)).map {|e| e.to_s } 74 | completions = call_search(@search, input, completions) if @search 75 | if @completion_prefix 76 | # Everything up to last break char stays on the line. 77 | # Must ensure only chars after break are prefixed 78 | @completion_prefix = @completion_prefix[/([^#{Readline::DefaultBreakCharacters}]+)$/,1] || '' 79 | completions = completions.map {|e| @completion_prefix + e } 80 | end 81 | completions 82 | end 83 | 84 | # Searches possible completions from the action which match the input. 85 | def call_search(search, input, list) 86 | Rc.send("#{search}_search", input || '', list) 87 | rescue 88 | message = $!.is_a?(NoMethodError) && !Rc.respond_to?("#{search}_search") ? 89 | "Completion search '#{search}' doesn't exist." : 90 | "Failed during completion search with '#{$!.message}'." 91 | raise FailedMissionError.new(self), message 92 | end 93 | 94 | # Calls the action to generate an array of possible completions. 95 | def call_action(input) 96 | @action.respond_to?(:call) ? @action.call(input) : Rc.send(@action, input) 97 | rescue StandardError, SyntaxError 98 | message = $!.is_a?(NoMethodError) && !@action.respond_to?(:call) && 99 | !Rc.respond_to?(@action) ? "Completion action '#{@action}' doesn't exist." : 100 | "Failed during completion action '#{name}' with '#{$!.message}'." 101 | raise FailedMissionError.new(self), message 102 | end 103 | 104 | # A message used to explains under what conditions a mission matched the user input. 105 | # Useful for spying and debugging. 106 | def match_message 107 | "Matches completion with condition #{condition.inspect}." 108 | end 109 | 110 | # A regexp representing the condition under which a mission matches the input. 111 | def condition 112 | self.class.const_defined?(:CONDITION) ? Regexp.new(self.class.const_get(:CONDITION)) : @on 113 | end 114 | 115 | # The name or generated unique_id for a mission. Mostly for use with Bond.recomplete. 116 | def name 117 | @name ? @name.to_s : unique_id 118 | end 119 | 120 | # Method which must return non-nil for a mission to match. 121 | def do_match(input) 122 | @matched = input.match(@on) 123 | end 124 | 125 | # Stuff a mission needs to do after matching successfully, in preparation for Mission.execute. 126 | def after_match(input) 127 | create_input(input[/\S+$/]) 128 | end 129 | 130 | private 131 | def condition_with_objects 132 | self.class.const_get(:CONDITION).sub('OBJECTS', self.class.const_get(:OBJECTS).join('|')) 133 | end 134 | 135 | def eval_object(obj) 136 | @evaled_object = self.class.current_eval(obj) 137 | true 138 | rescue Exception 139 | raise FailedMissionError.new(self), "Match failed during eval of '#{obj}'." if Bond.config[:eval_debug] 140 | false 141 | end 142 | 143 | def unique_id 144 | @on.inspect 145 | end 146 | 147 | def create_input(input, options={}) 148 | @input = Input.new(input, options.merge(:line => @line, :matched => @matched)) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/bond.rb: -------------------------------------------------------------------------------- 1 | require 'bond/m' 2 | require 'bond/version' 3 | require 'bond/readline' 4 | require 'bond/readlines/rawline' 5 | require 'bond/readlines/ruby' 6 | require 'bond/readlines/jruby' 7 | require 'bond/agent' 8 | require 'bond/search' 9 | require 'bond/input' 10 | require 'bond/rc' 11 | require 'bond/mission' 12 | require 'bond/missions/default_mission' 13 | require 'bond/missions/method_mission' 14 | require 'bond/missions/object_mission' 15 | require 'bond/missions/anywhere_mission' 16 | require 'bond/missions/operator_method_mission' 17 | 18 | module Bond 19 | extend self 20 | 21 | # Creates a completion rule (Mission). A valid Mission consists of a condition and an action. A 22 | # condition is specified with one of the following options: :on, :object, :anywhere or :method(s). Each 23 | # of these options creates a different Mission class. An action is either the method's block or :action. 24 | # An action takes what the user has typed (Input) and returns an array of possible completions. Bond 25 | # searches these completions and returns matching completions. This searching behavior can be configured 26 | # or turned off per mission with :search. If turned off, the action must also handle searching. 27 | # 28 | # ==== Examples: 29 | # Bond.complete(:method => 'shoot') {|input| %w{to kill} } 30 | # Bond.complete(:on => /^((([a-z][^:.\(]*)+):)+/, :search => false) {|input| Object.constants.grep(/#{input.matched[1]}/) } 31 | # Bond.complete(:object => ActiveRecord::Base, :search => :underscore, :place => :last) 32 | # Bond.complete(:method => 'you', :search => proc {|input, list| list.grep(/#{input}/i)} ) {|input| %w{Only Live Twice} } 33 | # Bond.complete(:method => 'system', :action => :shell_commands) 34 | # 35 | # @param [Hash] options When using :method(s) or :object, some hash keys may have different behavior. See 36 | # Bond.complete sections of {MethodMission} and {ObjectMission} respectively. 37 | # @option options [Regexp] :on Matches the full line of input to create a {Mission} object. 38 | # @option options [String] :method An instance (Class#method) or class method (Class.method). Creates 39 | # {MethodMission} object. A method's class can be set by :class or detected automatically if '#' or '.' is 40 | # present. If no class is detected, 'Kernel#' is assumed. 41 | # @option options [Array] :methods Instance or class method(s) in the format of :method. Creates 42 | # {MethodMission} objects. 43 | # @option options [String] :class Optionally used with :method or :methods to represent module/class. 44 | # Must end in '#' or '.' to indicate instance/class method. Suggested for use with :methods. 45 | # @option options [String] :object Module or class of an object whose methods are completed. Creates 46 | # {ObjectMission} object. 47 | # @option options [String] :anywhere String representing a regular expression to match a mission. Creates 48 | # {AnywhereMission} object. 49 | # @option options [String] :prefix Optional string to prefix :anywhere. 50 | # @option options [Symbol,false] :search Determines how completions are searched. Defaults to 51 | # Search.default_search. If false, search is turned off and assumed to be done in the action. 52 | # Possible symbols are :anywhere, :ignore_case, :underscore, :normal, :files and :modules. 53 | # See {Search} for more info. 54 | # @option options [String,Symbol] :action Rc method name that takes an Input and returns possible completions. 55 | # See {MethodMission} for specific behavior with :method(s). 56 | # @option options [Integer,:last] :place Indicates where a mission is inserted amongst existing 57 | # missions. If the symbol :last, places the mission at the end regardless of missions defined 58 | # after it. Multiple declarations of :last are kept last in the order they are defined. 59 | # @option options [Symbol,String] :name Unique id for a mission which can be passed by 60 | # Bond.recomplete to identify and replace the mission. 61 | def complete(options={}, &block); M.complete(options, &block); end 62 | 63 | # Redefines an existing completion mission to have a different action. The condition can only be varied if :name is 64 | # used to identify and replace a mission. Takes same options as {#complete}. 65 | # ==== Example: 66 | # Bond.recomplete(:on => /man/, :name => :count) { %w{4 5 6}} 67 | def recomplete(options={}, &block); M.recomplete(options, &block); end 68 | 69 | # Reports what completion mission matches for a given input. Helpful for debugging missions. 70 | # ==== Example: 71 | # >> Bond.spy "shoot oct" 72 | # Matches completion mission for method matching "shoot". 73 | # Possible completions: ["octopussy"] 74 | def spy(*args); M.spy(*args); end 75 | 76 | # @return [Hash] Global config 77 | def config; M.config; end 78 | 79 | # Starts Bond with a default set of completions that replace and improve irb's completion. Loads completions 80 | # in this order: lib/bond/completion.rb, lib/bond/completions/*.rb and the following optional completions: 81 | # completions from :gems, ~/.bondrc, ~/.bond/completions/*.rb and from block. See 82 | # {Rc} for the DSL to use in completion files and in the block. 83 | # 84 | # ==== Examples: 85 | # Bond.start :gems => %w{hirb} 86 | # Bond.start(:default_search => :ignore_case) do 87 | # complete(:method => "Object#respond_to?") {|e| e.object.methods } 88 | # end 89 | # 90 | # @param [Hash] options Sets global keys in {#config}, some which specify what completions to load. 91 | # @option options [Array] :gems Gems which have their completions loaded from 92 | # @gem_source/lib/bond/completions/*.rb. If gem is a plugin gem i.e. ripl-plugin, completion will be loaded 93 | # from @gem_source/lib/ripl/completions/plugin.rb. 94 | # @option options [Module, Symbol] :readline (Bond::Readline) Specifies a Bond readline plugin. 95 | # A symbol points to a capitalized Bond constant i.e. :ruby -> Bond::Ruby. 96 | # Available plugins are Bond::Readline, Bond::Ruby, Bond::Jruby and Bond::Rawline. 97 | # @option options [Proc] :default_mission (DefaultMission) Sets default completion to use when no missions match. 98 | # See {Agent#default_mission}. 99 | # @option options [Symbol] :default_search (:underscore) Name of a *_search method in Rc to use as the default 100 | # search in completions. See {#complete}'s :search option for valid values. 101 | # @option options [Binding, Proc] :eval_binding (TOPLEVEL_BINDING) Binding to use when evaluating objects in 102 | # ObjectMission and MethodMission. When in irb, defaults to irb's current binding. When proc, 103 | # binding is evaluated each time by calling proc. 104 | # @option options [Boolean] :debug (false) Shows the stacktrace when autocompletion fails and raises exceptions 105 | # in Rc.eval. 106 | # @option options [Boolean] :eval_debug (false) Raises eval errors occuring when finding a matching completion. 107 | # Useful to debug an incorrect completion 108 | # @option options [Boolean] :bare (false) Doesn't load default ruby completions and completions in 109 | # ~/.bond*. Useful for non-ruby completions 110 | def start(options={}, &block); M.start(options, &block); end 111 | 112 | # Restarts completions with given options, ensuring to delete current completions. 113 | # Takes same options as Bond#start. 114 | def restart(options={}, &block); M.restart(options, &block); end 115 | 116 | # Indicates if Bond has already started 117 | def started?; M.started?; end 118 | 119 | # Loads completions for gems that ship with them under lib/bond/completions/, relative to the gem's base directory. 120 | def load_gems(*gems); M.load_gems(*gems); end 121 | 122 | # An Agent who saves all Bond.complete missions and executes the correct one when a completion is called. 123 | def agent; M.agent; end 124 | 125 | # Lists all methods that have argument completion. 126 | def list_methods; MethodMission.all_methods; end 127 | end 128 | -------------------------------------------------------------------------------- /test/method_mission_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "method mission" do 4 | before_all { MethodMission.reset } 5 | before { Bond.agent.reset; Bond.complete(:all_methods=>true) } 6 | 7 | describe "method" do 8 | before { complete(:method=>'Array#index') {|e| %w{ab cd ef ad} } } 9 | 10 | it "completes" do 11 | tab('[].index c').should == %w{cd} 12 | end 13 | 14 | it "completes quoted argument" do 15 | tab('[].index "a').should == %w{ab ad} 16 | end 17 | 18 | it "completes parenthetical argument" do 19 | tab('[].index("a').should == %w{ab ad} 20 | end 21 | 22 | it "completes symbolic argument" do 23 | tab('[].index :a').should == %w{:ab :ad} 24 | end 25 | 26 | it "needs space to complete argument" do 27 | tab('[].indexa').should == [] 28 | tab('[].index a').should == %w{ab ad} 29 | end 30 | 31 | it "completes all arguments with only space as argument" do 32 | tab('[].index ').should == %w{ab cd ef ad} 33 | end 34 | 35 | it "completes with a chain of objects" do 36 | tab('{}.to_a.index a').should == %w{ab ad} 37 | end 38 | 39 | it "completes after valid ruby" do 40 | tab('nil; [].index a').should == %w{ab ad} 41 | end 42 | 43 | it "completes after invalid ruby" do 44 | tab('blah [].index a').should == %w{ab ad} 45 | end 46 | 47 | describe "completes for whitespaced object that is" do 48 | def complete_and_tab(klass, example) 49 | complete(:method=>"#{klass}#to_s") { %w{ab cd ef ad} } 50 | tab(example + '.to_s a').should == %w{ab ad} 51 | end 52 | 53 | it "a string" do 54 | complete_and_tab(String, "'man oh'") 55 | end 56 | 57 | it "an array" do 58 | complete_and_tab(Array, "[1, 2, 3]") 59 | end 60 | 61 | it "a hash" do 62 | complete_and_tab(Hash, "{:a => 1}") 63 | end 64 | 65 | it "a regexp" do 66 | complete_and_tab(Regexp, "/man oh/") 67 | end 68 | 69 | it "a proc" do 70 | complete_and_tab(Proc, "lambda { }") 71 | complete_and_tab(Proc, "proc { }") 72 | end 73 | 74 | it "a range" do 75 | complete_and_tab(Range, "(1.. 10)") 76 | end 77 | 78 | it "wrapped ()" do 79 | complete_and_tab(Fixnum, "(2 * 2)") 80 | end 81 | 82 | it "quoted by {}" do 83 | complete_and_tab(String, "%q{man oh}") 84 | end 85 | 86 | it "quoted by []" do 87 | complete_and_tab(String, "%q[man oh]") 88 | end 89 | end 90 | end 91 | 92 | describe "any instance method" do 93 | before { complete(:method=>'Array#index', :search=>:anywhere) {|e| %w{ab cd ef ad} } } 94 | 95 | it "completes for objects of a subclass" do 96 | class ::MyArray < Array; end 97 | tab('MyArray.new.index a').should == %w{ab ad} 98 | end 99 | 100 | it "completes for objects of a subclass using its own definition" do 101 | class ::MyArray < Array; end 102 | complete(:method=>'MyArray#index') {|e| %w{aa ab bc} } 103 | tab('MyArray.new.index a').should == %w{aa ab} 104 | end 105 | 106 | it "ignores invalid ruby" do 107 | tab("[{].index a").should == [] 108 | end 109 | 110 | it "with string :action copies its action" do 111 | complete(:method=>"Array#fetch", :action=>"Array#index") 112 | tab('[].fetch a').should == %w{ab ad} 113 | end 114 | 115 | it "with string :action copies its search" do 116 | complete(:method=>"Array#fetch", :action=>"Array#index") 117 | tab('[].fetch d').should == %w{cd ad} 118 | end 119 | 120 | it "with string :action and :search doesn't copy its search" do 121 | complete(:method=>"Array#fetch", :action=>"Array#index", :search=>:normal) 122 | tab('[].fetch d').should == [] 123 | end 124 | 125 | it "with symbol :action references Rc method" do 126 | Rc.module_eval %[def _fetch(input); %w{ab cd ef ad}; end ] 127 | complete(:method=>"Array#fetch", :action=>:_fetch) 128 | tab('[].fetch a').should == %w{ab ad} 129 | end 130 | end 131 | 132 | describe "any class method" do 133 | before { complete(:method=>'Date.parse') {|e| %w{12/01 03/01 01/01} } } 134 | 135 | it "completes" do 136 | tab('Date.parse 0').should == ["03/01", "01/01"] 137 | end 138 | 139 | it "completes for a subclass using inherited definition" do 140 | class ::MyDate < Date; end 141 | tab('MyDate.parse 0').should == ["03/01", "01/01"] 142 | end 143 | 144 | it "completes for a subclass using its own definition" do 145 | class ::MyDate < Date; end 146 | complete(:method=>'MyDate.parse') {|e| %w{12 03 01} } 147 | tab('MyDate.parse 0').should == %w{03 01} 148 | end 149 | 150 | it "with string :action copies existing action" do 151 | complete(:method=>"Date.blah", :action=>"Date.parse") 152 | tab('Date.blah 0').should == ["03/01", "01/01"] 153 | end 154 | 155 | it "doesn't conflict with instance method completion" do 156 | complete(:method=>'Date#parse') {|e| %w{01 02 23}} 157 | tab('Date.today.parse 0').should == %w{01 02} 158 | end 159 | end 160 | 161 | describe "multi argument method" do 162 | before { complete(:method=>'Array#index') {|e| %w{ab cd ef ad e,e} } } 163 | 164 | it "completes second argument" do 165 | tab('[].index ab, a').should == %w{ab ad} 166 | end 167 | 168 | it "completes second argument as a symbol" do 169 | tab('[].index ab, :a').should == %w{:ab :ad} 170 | end 171 | 172 | it "completes second argument as a string" do 173 | tab('[].index \'ab\' , "a').should == %w{ab ad} 174 | end 175 | 176 | it "completes third argument" do 177 | tab('[].index ab, zz, c').should == %w{cd} 178 | end 179 | 180 | it "completes all arguments after comma" do 181 | tab('[].index ab,').should == %w{ab cd ef ad e,e} 182 | tab('[].index ab, ').should == %w{ab cd ef ad e,e} 183 | end 184 | 185 | it "completes based on argument number" do 186 | complete(:method=>'blah') {|e| e.argument == 2 ? %w{ab ad} : %w{ab ae} } 187 | tab('blah a').should == %w{ab ae} 188 | tab('blah zz, a').should == %w{ab ad} 189 | end 190 | 191 | it "can't handle a completion with a comma as a completion" do 192 | tab('[].index e,').should == %w{ab cd ef ad e,e} 193 | end 194 | end 195 | 196 | describe "top-level method" do 197 | before { complete(:method=>'cool') {|e| %w{ab cd ef ad} } } 198 | 199 | it "completes" do 200 | complete(:method=>'cool?') {|e| [] } 201 | tab('cool c').should == %w{cd} 202 | end 203 | 204 | it "completes after valid ruby" do 205 | tab('nil; cool a').should == %w{ab ad} 206 | end 207 | end 208 | 209 | it "with :methods completes for multiple instance methods" do 210 | complete(:methods=>%w{cool ls}) {|e| %w{ab cd ef ad}} 211 | tab("cool a").should == %w{ab ad} 212 | tab("ls c").should == %w{cd} 213 | end 214 | 215 | it "with :methods completes for instance and class methods" do 216 | complete(:methods=>%w{String#include? String.new}) {|e| %w{ab cd ef ad}} 217 | tab("'blah'.include? a").should == %w{ab ad} 218 | tab("String.new a").should == %w{ab ad} 219 | end 220 | 221 | it "with :search completes" do 222 | complete(:method=>"blah", :search=>:anywhere) { %w{abc bcd cde} } 223 | tab('blah bc').should == ['abc', 'bcd'] 224 | end 225 | 226 | describe "with :class" do 227 | it "completes for instance methods" do 228 | complete(:method=>"blong", :class=>"Array#") { %w{ab cd ef ad} } 229 | tab('[].blong a').should == %w{ab ad} 230 | complete(:methods=>["bling"], :class=>"Array#") { %w{ab cd ef ad} } 231 | tab('[].bling a').should == %w{ab ad} 232 | end 233 | 234 | it "that is ambiguous defaults to instance methods" do 235 | complete(:method=>"blong", :class=>"Array") { %w{ab cd ef ad} } 236 | tab('[].blong a').should == %w{ab ad} 237 | end 238 | 239 | it "completes for class methods" do 240 | complete(:method=>"blong", :class=>"Array.") { %w{ab cd ef ad} } 241 | tab('Array.blong a').should == %w{ab ad} 242 | complete(:methods=>["bling"], :class=>"Array.") { %w{ab cd ef ad} } 243 | tab('Array.bling a').should == %w{ab ad} 244 | end 245 | end 246 | end -------------------------------------------------------------------------------- /lib/bond/missions/method_mission.rb: -------------------------------------------------------------------------------- 1 | module Bond 2 | # A mission which completes arguments for any module/class method that isn't an operator method. 3 | # To create this mission or OperatorMethodMission, :method or :methods must be passed to Bond.complete. 4 | # A completion for a given module/class effects any object that has it as an ancestor. If an object 5 | # has two ancestors that have completions for the same method, the ancestor closer to the object is 6 | # picked. For example, if Array#collect and Enumerable#collect have completions, argument completion on 7 | # '[].collect ' would use Array#collect. 8 | # 9 | # ==== Bond.complete Options: 10 | # [:action] If a string, value is assumed to be a :method and that method's action is copied. 11 | # Otherwise defaults to normal :action behavior. 12 | # [:search] If :action is a :method string, defaults to copying its search. 13 | # Otherwise defaults to normal :search behavior. 14 | # [:name, :place] These options aren't supported by a MethodMission/OperatorMethodMission completion. 15 | # ==== Examples: 16 | # Bond.complete(:methods => %w{delete index rindex}, :class => "Array#") {|e| e.object } 17 | # Bond.complete(:method => "Hash#index") {|e| e.object.values } 18 | # 19 | # ==== Argument Format 20 | # All method arguments can autocomplete as symbols or strings and the first argument can be prefixed 21 | # with '(': 22 | # >> Bond.complete(:method => 'example') { %w{some example eh} } 23 | # => true 24 | # >> example '[TAB] 25 | # eh example some 26 | # >> example :[TAB] 27 | # :eh :example :some 28 | # 29 | # >> example("[TAB] 30 | # eh example some 31 | # 32 | # ==== Multiple Arguments 33 | # Every time a comma appears after a method, Bond starts a new completion. This allows a method to 34 | # complete multiple arguments as well as complete keys for a hash. *Each* argument can be have a unique 35 | # set of completions since a completion action is aware of what argument it is currently completing: 36 | # >> Bond.complete(:method => 'FileUtils.chown') {|e| 37 | # e.argument > 3 ? %w{noop verbose} : %w{root admin me} } 38 | # => true 39 | # >> FileUtils.chown 'r[TAB] 40 | # >> FileUtils.chown 'root' 41 | # >> FileUtils.chown 'root', 'a[TAB] 42 | # >> FileUtils.chown 'root', 'admin' 43 | # >> FileUtils.chown 'root', 'admin', 'some_file', :v[TAB] 44 | # >> FileUtils.chown 'root', 'admin', 'some_file', :verbose 45 | # >> FileUtils.chown 'root', 'admin', 'some_file', :verbose => true 46 | # 47 | # ==== Developer Notes 48 | # Unlike other missions, creating these missions with Bond.complete doesn't add more completion rules 49 | # for an Agent to look through. Instead, all :method(s) completions are handled by one MethodMission 50 | # object which looks them up with its own hashes. In the same way, all operator methods are 51 | # handled by one OperatorMethodMission object. 52 | class MethodMission < Bond::Mission 53 | class< get_class(b[0]) || -1 }. 140 | find {|k,v| obj.send(find_meth, get_class(k)) } 141 | end 142 | 143 | def get_class(klass) 144 | (@klasses ||= {})[klass] ||= any_const_get(klass) 145 | end 146 | end 147 | 148 | self.reset 149 | OBJECTS = Mission::OBJECTS + %w{\S*?} 150 | CONDITION = %q{(OBJECTS)\.?(METHODS)(?:\s+|\()(['":])?(.*)$} 151 | 152 | def match_message #@private 153 | "Matches completion for method '#{@meth}' in '#{MethodMission.last_class}'." 154 | end 155 | 156 | protected 157 | def do_match(input) 158 | (@on = default_on) && super && eval_object(@matched[1] ? @matched[1] : 'self') && 159 | MethodMission.find(@evaled_object, @meth = matched_method) 160 | end 161 | 162 | def default_on 163 | Regexp.new condition_with_objects.sub('METHODS',Regexp.union(*current_methods).to_s) 164 | end 165 | 166 | def current_methods 167 | self.class.action_methods - OPERATORS 168 | end 169 | 170 | def default_action 171 | MethodMission.last_find[0] 172 | end 173 | 174 | def matched_method 175 | @matched[2] 176 | end 177 | 178 | def set_action_and_search 179 | @action = default_action 180 | @search = MethodMission.last_find[1] || Search.default_search 181 | end 182 | 183 | def after_match(input) 184 | set_action_and_search 185 | @completion_prefix, typed = @matched[3], @matched[-1] 186 | input_options = {:object => @evaled_object, :argument => 1+typed.count(','), 187 | :arguments => (@completion_prefix.to_s+typed).split(/\s*,\s*/) } 188 | if typed.to_s.include?(',') && (match = typed.match(/(.*?\s*)([^,]*)$/)) 189 | typed = match[2] 190 | typed.sub!(/^(['":])/,'') 191 | @completion_prefix = typed.empty? ? '' : "#{@matched[3]}#{match[1]}#{$1}" 192 | end 193 | create_input typed, input_options 194 | end 195 | 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /test/agent_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Agent" do 4 | describe "#call" do 5 | before { Bond.agent.reset } 6 | 7 | it "chooses default mission if no missions match" do 8 | complete(:on=>/bling/) {|e| [] } 9 | Bond.agent.default_mission.expects(:execute).with {|e| e.is_a?(Input) } 10 | tab 'blah' 11 | end 12 | 13 | it "for internal Bond error completes error" do 14 | complete(:on=>/bling/) {|e| [] } 15 | Bond.agent.expects(:find_mission).raises('wtf') 16 | errors = tab('bling') 17 | errors[0].should =~ /Bond Error: Failed internally.*'wtf'/ 18 | errors[1].should =~ /Please/ 19 | end 20 | 21 | it "allows the readline buffer to be provided as an argument" do 22 | Bond.agent.weapon.stubs(:line_buffer).raises(Exception) 23 | should.not.raise { Bond.agent.call('bl', 'bl foo') } 24 | end 25 | 26 | def complete_error(msg) 27 | lambda {|e| 28 | e[0].should =~ msg 29 | e[1].should =~ /Completion Info: Matches.*blah/ 30 | } 31 | end 32 | 33 | it "for completion action raising error completes error" do 34 | Bond.config[:debug] = false 35 | complete(:on=>/blah/) { raise 'blah' } 36 | errors = tab('blah') 37 | errors.size.should == 2 38 | errors.should complete_error(/Bond Error: Failed.*action.*'blah'/) 39 | Bond.config[:debug] = true 40 | end 41 | 42 | it "for completion action raising error with debug completes error and stacktrace" do 43 | complete(:on=>/blah/) { raise 'blah' } 44 | errors = tab('blah') 45 | errors.size.should == 3 46 | errors.should complete_error(/Bond Error: Failed.*action.*'blah'/) 47 | errors[2].should =~ /Stack Trace:/ 48 | end 49 | 50 | it "for completion action raising NoMethodError completes error" do 51 | complete(:on=>/blah/) { raise NoMethodError } 52 | tab('blah').should complete_error(/Bond Error: Failed.*action.*'NoMethodError'/) 53 | end 54 | 55 | it 'for completion action failing with Rc.eval completes empty' do 56 | Bond.config[:debug] = false 57 | complete(:on=>/blah/) { Rc.eval '{[}'} 58 | tab('blah').should == [] 59 | Bond.config[:debug] = true 60 | end 61 | 62 | it 'for completion action failing with Rc.eval and debug completes error' do 63 | complete(:on=>/blah/) { Rc.eval('{[}') || [] } 64 | tab('blah').should complete_error(/Bond Error: Failed.*action.*(syntax|expect)/m) 65 | end 66 | 67 | it "for completion action raising SyntaxError in eval completes error" do 68 | complete(:on=>/blah/) { eval '{[}'} 69 | tab('blah').should complete_error(/Bond Error: Failed.*action.*(eval)/) 70 | end 71 | 72 | it "for completion action that doesn't exist completes error" do 73 | complete(:on=>/blah/, :action=>:huh) 74 | tab('blah').should complete_error(/Bond Error:.*action 'huh' doesn't exist/) 75 | end 76 | 77 | it "for completion search raising error completes error" do 78 | Rc.module_eval "def blah_search(*args); raise 'blah'; end" 79 | complete(:on=>/blah/, :search=>:blah) { [1] } 80 | tab('blah').should complete_error(/Bond Error: Failed.*search.*'blah'/) 81 | end 82 | 83 | it "for completion search that doesn't exist completes error" do 84 | complete(:on=>/blah/, :search=>:huh) { [] } 85 | tab('blah').should complete_error(/Bond Error:.*search 'huh' doesn't exist/) 86 | end 87 | end 88 | 89 | describe "complete" do 90 | before {|e| Bond.agent.reset } 91 | def complete_prints_error(msg, *args, &block) 92 | capture_stderr { complete(*args, &block) }.should =~ msg 93 | end 94 | 95 | it "with no :action prints error" do 96 | complete_prints_error /Invalid :action/, :on=>/blah/ 97 | end 98 | 99 | it "with no :on prints error" do 100 | complete_prints_error(/Invalid :on/) { [] } 101 | end 102 | 103 | it "with invalid :on prints error" do 104 | complete_prints_error(/Invalid :on/, :on=>'blah') { [] } 105 | end 106 | 107 | it "with internal failure prints error" do 108 | Mission.expects(:create).raises(RuntimeError, 'blah') 109 | complete_prints_error(/Unexpected error.*blah/, :on=>/blah/) { [] } 110 | end 111 | 112 | it "with invalid :anywhere and :prefix prints no error" do 113 | complete_prints_error(/^$/, :prefix=>nil, :anywhere=>:blah) {} 114 | end 115 | 116 | it "with invalid :object prints no error" do 117 | complete_prints_error(/^$/, :object=>:Mod) {} 118 | end 119 | 120 | it "with invalid :method prints error" do 121 | complete_prints_error(/Invalid.*:method\(s\)/, :method=>true) {} 122 | end 123 | 124 | it "with invalid array :method prints error" do 125 | complete_prints_error(/Invalid array :method/, :method=>%w{one two}) {} 126 | end 127 | 128 | it "with invalid :methods prints error" do 129 | complete_prints_error(/Invalid.*:method\(s\)/, :methods=>[:blah]) {} 130 | end 131 | 132 | it "with invalid :action for method completion prints error" do 133 | complete_prints_error(/Invalid string :action/, :method=>"blah", :action=>"Kernel#wtf") {} 134 | end 135 | 136 | it "with invalid :class prints no error" do 137 | complete_prints_error(/^$/, :method=>'ok', :class=>/wtf/) {} 138 | end 139 | 140 | it "places missions last when declared last" do 141 | complete(:object=>"Symbol", :place=>:last) 142 | complete(:on=>/man/, :place=>:last) { } 143 | complete(:on=>/man\s*(.*)/) {|e| [e.matched[1]] } 144 | Bond.agent.missions.map {|e| e.class}.should == [Mission, ObjectMission, Mission] 145 | tab('man ok').should == ['ok'] 146 | end 147 | 148 | it "places mission correctly for a place number" do 149 | complete(:object=>"Symbol") 150 | complete(:on=>/man/) {} 151 | complete(:on=>/man\s*(.*)/, :place=>1) {|e| [e.matched[1]] } 152 | tab('man ok') 153 | Bond.agent.missions.map {|e| e.class}.should == [Mission, ObjectMission, Mission] 154 | tab('man ok').should == ['ok'] 155 | end 156 | end 157 | 158 | describe "recomplete" do 159 | before {|e| Bond.agent.reset } 160 | 161 | it "recompletes a mission" do 162 | complete(:on=>/man/) { %w{1 2 3}} 163 | Bond.recomplete(:on=>/man/) { %w{4 5 6}} 164 | tab('man ').should == %w{4 5 6} 165 | end 166 | 167 | it "recompletes a mission with :name" do 168 | complete(:on=>/man/, :name=>:count) { %w{1 2 3}} 169 | Bond.recomplete(:on=>/man/, :name=>:count) { %w{4 5 6}} 170 | tab('man ').should == %w{4 5 6} 171 | end 172 | 173 | it "recompletes a method mission" do 174 | complete(:all_methods=>true) 175 | MethodMission.reset 176 | complete(:method=>'blah') { %w{1 2 3}} 177 | Bond.recomplete(:method=>'blah') { %w{4 5 6}} 178 | tab('blah ').should == %w{4 5 6} 179 | end 180 | 181 | it "completes a method mission if mission not found" do 182 | complete(:all_methods=>true) 183 | MethodMission.reset 184 | Bond.recomplete(:method=>'blah') { %w{4 5 6}} 185 | tab('blah ').should == %w{4 5 6} 186 | end 187 | 188 | it "recompletes an object mission" do 189 | complete(:object=>'String') { %w{1 2 3}} 190 | Bond.recomplete(:object=>'String') { %w{4 5 6}} 191 | tab('"blah".').should == %w{.4 .5 .6} 192 | end 193 | 194 | it "recompletes anywhere mission" do 195 | complete(:anywhere=>'dude.*') { %w{duder dudest} } 196 | Bond.recomplete(:anywhere=>'dude.*') { %w{duderific dudeacious} } 197 | tab('dude').should == %w{duderific dudeacious} 198 | end 199 | 200 | it "prints error if no existing mission" do 201 | complete(:object=>'String') { %w{1 2 3}} 202 | capture_stderr { Bond.recomplete(:object=>'Array') { %w{4 5 6}}}.should =~ /No existing mission/ 203 | tab('[].').should == [] 204 | end 205 | 206 | it "prints error if invalid condition given" do 207 | capture_stderr { Bond.recomplete}.should =~ /Invalid :action/ 208 | end 209 | end 210 | 211 | describe "spy" do 212 | before_all { 213 | reset 214 | complete(:on=>/end$/) { [] }; 215 | complete(:all_methods=>true); complete(:method=>'the') { %w{spy who loved me} } 216 | complete(:object=>"Symbol") 217 | } 218 | 219 | it "detects basic mission" do 220 | capture_stdout { Bond.spy('the end')}.should =~ /end/ 221 | end 222 | 223 | it "detects object mission" do 224 | capture_stdout { Bond.spy(':dude.i')}.should =~ /object.*Symbol.*dude\.id/m 225 | end 226 | 227 | it "detects method mission" do 228 | capture_stdout { Bond.spy('the ')}.should =~ /method.*the.*Kernel.*loved/m 229 | end 230 | 231 | it "detects no mission" do 232 | capture_stdout { Bond.spy('blah')}.should =~ /Doesn't match/ 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == Description 2 | 3 | Bond is on a mission to improve autocompletion in ruby, especially for irb/ripl. Aside from doing 4 | everything irb's can do and fixing its quirks, Bond can autocomplete argument(s) to methods, 5 | uniquely completing per module, per method and per argument. Bond brings ruby autocompletion closer 6 | to bash/zsh as it provides a configuration system and a DSL for creating custom completions and 7 | completion rules. With this configuration system, users can customize their autocompletions and 8 | share it with others. Bond can also load completions that ship with gems. Bond is able to offer 9 | more than irb's completion since it uses the full line of input when completing as opposed to irb's 10 | last-word approach. 11 | 12 | == Install 13 | 14 | Note: Bond is only supported on platforms with make e.g. OSX, Linux and Cygwin Windows. Once 1.8 15 | support is dropped this won't be an issue. 16 | 17 | To use bond with {Readline}[http://cnswww.cns.cwru.edu/php/chet/readline/rltop.html] (version >= 18 | 5.6 recommended) or JLine for JRuby users, install the gem with: 19 | 20 | gem install bond 21 | 22 | To use bond with {a pure ruby readline}[https://github.com/luislavena/rb-readline] i.e. Windows 23 | users or users without Readline: 24 | 25 | gem install bond rb-readline -- --without-readline 26 | 27 | To use bond without readline support (and presumably use your own readline plugin): 28 | 29 | gem install bond -- --without-readline 30 | 31 | == Setup 32 | 33 | If you're using {ripl}[https://github.com/cldwalker/ripl] instead of irb, bond is already setup. 34 | 35 | To start off, replace irb's completion (require 'irb/completion') with Bond's enhanced version in your irbrc: 36 | 37 | require 'bond' 38 | Bond.start 39 | # For users using a pure ruby readline 40 | Bond.start :readline => :ruby 41 | 42 | This setup gives you more consistent method completion on any object, customizable completions 43 | and argument completion of some 80+ methods including Hash#[], Kernel#system, Kernel#require and some Rails methods. 44 | 45 | == Method Argument Completion 46 | 47 | By default, Bond autocompletes arguments for a number of core methods: 48 | 49 | $ ripl 50 | # require completes gems and anything in $LOAD_PATH 51 | >> require 'rb[TAB] 52 | rbconfig.rb rbconfig/ 53 | >> require 'rbconfig 54 | >> require 'rbconfig.rb' 55 | 56 | # hash methods can complete their keys 57 | >> CONFIG::CONFIG[TAB] 58 | >> CONFIG::CONFIG['m[TAB] 59 | >> CONFIG::CONFIG['mandir' 60 | >> CONFIG::CONFIG['mandir'] 61 | 62 | >> ENV['CO[TAB] 63 | COLUMNS COMMAND_MODE 64 | >> ENV['COL[TAB] 65 | >> ENV['COLUMNS' 66 | >> ENV['COLUMNS'] 67 | 68 | # array methods can complete their elements 69 | >> %w{ab bc cd de}.delete '[TAB] 70 | ab bc cd de 71 | >> %w{ab bc cd de}.delete 'a[TAB] 72 | >> %w{ab bc cd de}.delete 'ab' 73 | 74 | # system can complete shell commands 75 | >> system 'ec[TAB] 76 | >> system 'echo 77 | >> system 'echo' 78 | 79 | Bond also comes with some basic Rails completions, mostly for attributes/columns of models: 80 | 81 | $ script/console 82 | >> Url.column_names 83 | => ["id", "name", "description", "created_at", "updated_at"] 84 | >> Url.create :n[TAB] 85 | >> Url.create :name 86 | ... 87 | >> Url.first.update_attribute :d[TAB] 88 | >> Url.first.update_attribute :description 89 | ... 90 | 91 | To see more methods whose arguments can be completed: 92 | >> puts Bond.list_methods 93 | ActiveRecord::Base#[] 94 | ActiveRecord::Base#attribute_for_inspect 95 | ... 96 | 97 | == Multiple Arguments 98 | Every time a comma appears after a method, Bond starts a new completion. This allows a method to 99 | complete multiple arguments. *Each* argument can be have a unique set of completions since a completion action 100 | is aware of what argument it is currently completing. Take for example the completion for Object#send: 101 | 102 | >> Bond.send :me[TAB] 103 | >> Bond.send :method 104 | >> Bond.send :method, [TAB] 105 | agent complete config recomplete spy start 106 | >> Bond.send :method, :a[TAB] 107 | >> Bond.send :method, :agent 108 | => # 109 | 110 | Notice the arguments were completed differently: the first completing for Bond.send and the second for Bond.method. The second 111 | argument was only able to complete because there's a completion for Module#method. Using Object#send it's possible to 112 | use completions defined for private methods i.e. Module#remove_const: 113 | >> Bond.send :remove_const, :A[TAB] 114 | :Agent :AnywhereMission 115 | >> Bond.send :remove_const, :Ag[TAB] 116 | >> Bond.send :remove_const, :Agent 117 | 118 | Since Bond uses a comma to delimit completions, methods whose last argument is a hash can have their hash keys 119 | autocompleted. Revisiting the above Rails example: 120 | >> Url.create :n[TAB] 121 | >> Url.create :name 122 | >> Url.create :name=>'example.com', :d[TAB] 123 | >> Url.create :name=>'example.com', :description 124 | ... 125 | >> Url.first.update_attributes :d[TAB] 126 | >> Url.first.update_attributes :description 127 | >> Url.first.update_attributes :description=>'zzz', :u[TAB] 128 | >> Url.first.update_attributes :description=>'zzz', :updated_at 129 | ... 130 | 131 | == Creating Completions 132 | Bond's completion resembles bash/zsh's. When Bond.start is called, Bond looks up completion files in multiple places: 133 | ~/.bondrc and ~/.bond/completions/*.rb. Here's how bash and bond completion definitions compare in their config files: 134 | # Bash 135 | complete -W "one two three" example 136 | complete -F _example example 137 | 138 | # Bond 139 | complete(:method=>'example') { %w{one two three} } 140 | complete(:method=>'example', :action=>'_example') 141 | 142 | To read up on the wealth of completion types one can make, see the docs for Bond.complete. 143 | 144 | === Creating Argument Completions for Methods 145 | While the above method completion was a static list, most completions will dynamically generate completions based on the method's 146 | receiver (object). Let's look at such an example with Hash#[] : 147 | complete(:method=>"Hash#[]") {|e| e.object.keys } 148 | 149 | As you can see, the currently typed object is available as the :object attribute of the block's argument, a Bond::Input object. 150 | This object can offer other useful attributes describing what the user has typed. For example, the :argument attribute holds the 151 | current argument number being completed. Here's a completion that uses this attribute to complete differently for the first argument 152 | and remaining arguments: 153 | complete(:method=>'example') {|e| e.argument > 1 ? %w{verbose force noop} : %w{one two three} } 154 | 155 | === Creating Other Completions 156 | First you should know Bond works: A user creates completion missions with Bond.start and its config files (which are just 157 | Bond.complete calls). When a user autocompletes, Bond.agent looks up missions in the *order* they were defined and completes 158 | with the first one that matches. The exception to this ordering are :method completions. 159 | 160 | To create a completion, Bond.complete needs a regexp to match the user input and an action to generate completions when 161 | it matches. If the completion isn't working, use Bond.spy to see which completion is executing. If a completion needs to be placed 162 | before existing completions, use the :place option. 163 | 164 | == Irb's Incorrect Completions 165 | 166 | There are a number of incorrect completions irb gives for object methods. Bond fixes all of the ones described below. 167 | 168 | Irb completes anything surrounded with '{}' the same: 169 | 170 | $ irb 171 | >> proc {}.c[TAB] 172 | }.call }.class }.clear }.clone }.collect 173 | >> %w{ab bc}.c[TAB] 174 | }.call }.class }.clear }.clone }.collect 175 | >> %r{ab bc}.c[TAB] 176 | }.call }.class }.clear }.clone }.collect 177 | >> {}.c[TAB] 178 | }.call }.class }.clear }.clone }.collect 179 | >> {}.call 180 | NoMethodError: undefined method `call' for {}:Hash 181 | from (irb):1 182 | 183 | There are a number of cases where irb gives a default completion because it doesn't know what else to do. 184 | # The default completion 185 | >> self.[TAB] 186 | Display all 496 possibilities? (y or n) 187 | 188 | # And all of these cases are apparently the same: 189 | >> nil.[TAB] 190 | Display all 496 possibilities? (y or n) 191 | >> false.[TAB] 192 | Display all 496 possibilities? (y or n) 193 | >> true.[TAB] 194 | Display all 496 possibilities? (y or n) 195 | # Regular expressions with spaces 196 | >> /man oh man/.[TAB] 197 | Display all 496 possibilities? (y or n) 198 | # Grouped expressions 199 | >> (3 + 4).[TAB] 200 | Display all 496 possibilities? (y or n) 201 | 202 | # Nested hashes and arrays 203 | >> {:a=>{:a=>1}}.[TAB] 204 | Display all 496 possibilities? (y or n) 205 | >> [[1,2], [3,4]].[TAB] 206 | Display all 496 possibilities? (y or n) 207 | 208 | # Any object produced from a method call 209 | >> 'awesome'.to_sym.[TAB] 210 | Display all 496 possibilities? (y or n) 211 | >> :dude.to_s.[TAB] 212 | Display all 496 possibilities? (y or n) 213 | 214 | Ranges don't get much love 215 | >> (2..4).[TAB] 216 | # Nothing happens 217 | 218 | == Limitations 219 | If on a Mac and using Editline as a Readline replacement (Readline::VERSION =~ /editline/i), Bond will probably not work consistently. I strongly recommend switching to the official Readline. If using rvm, {this post}[http://niwos.com/2010/03/19/rvm-on-osx-snow-leopard-readline-errors/] 220 | has good instructions for reinstalling ruby with the official Readline. 221 | 222 | == Credits 223 | * Csaba Hank for {providing the C extension}[http://www.creo.hu/~csaba/ruby/irb-enhancements/doc/files/README.html] which Bond uses to read Readline's full buffer. 224 | * Takao Kouji for {commiting}[http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/ext/readline/readline.c?view=diff&r1=24018&r2=24019] this Readline enhancement to ruby 1.9.2. 225 | * pd for compatibility with emacs' inf-ruby mode. 226 | * timcharper for improving extconf.rb. 227 | * headius and rking for jruby help 228 | * ConradIrwin for 2.0 and other fixes 229 | * tobias for a java version of the gem 230 | * havenwood for 2.1 fixes 231 | * yui-knk, rev112 and dre-hh for bug fixes. 232 | 233 | == Bugs/Issues 234 | Please report them {on github}[http://github.com/cldwalker/bond/issues]. 235 | 236 | == Contributing 237 | {See here}[http://tagaholic.me/contributing.html] 238 | 239 | == Links 240 | * http://tagaholic.me/2010/05/07/screencast-of-argument-autocompletion-for-methods-in-irb.html 241 | * http://tagaholic.me/2009/07/16/bond-from-irb-with-completion-love.html 242 | * http://tagaholic.me/2009/07/22/better-irb-completion-with-bond.html 243 | * http://tagaholic.me/2009/07/23/mini-irb-and-mini-script-console.html 244 | 245 | == Todo 246 | * Make completion actions more synonymous with argument types. 247 | * Cache expensive completion actions. 248 | * Ensure completions work when there is additional, unrelated text to the right of a completion. 249 | --------------------------------------------------------------------------------