bond 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ == 0.1.0
2
+ * Intial release. Whoop!
@@ -0,0 +1,22 @@
1
+ The MIT LICENSE
2
+
3
+ Copyright (c) 2009 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.
@@ -0,0 +1,191 @@
1
+ == Description
2
+
3
+ Bond is on a mission to make custom autocompletion easy in irb and other console/readline-like
4
+ environments. Bond supports custom argument completion of methods, method completion of objects and
5
+ anything else your wicked regex's can do. Bond comes armed with a Readline C extension to get the
6
+ full line of input as opposed to irb's last-word based completion. Bond makes custom searching of
7
+ possible completions easy which allows for nontraditional ways of autocompleting i.e. instant
8
+ aliasing of multi worded methods.
9
+
10
+ == Install
11
+
12
+ Install the gem with:
13
+
14
+ sudo gem install cldwalker-bond --source http://gems.github.com
15
+
16
+ == Usage
17
+
18
+ === Argument Completion for Methods
19
+
20
+ bash> irb -rirb/completion -rubygems
21
+ # This loads Bond but it doesn't take over completion yet.
22
+ >> require 'bond'
23
+
24
+ # For Bond to handle completions, we must explicitly define a completion mission.
25
+ # Order matters since the order they're declared in is the order they're searched.
26
+ >> Bond.complete(:method=>'method1') {|input| %w{some args to autocomplete} }
27
+ => true
28
+ >> Bond.complete(:method=>'method2') {|input| %w{more args to autocomplete} }
29
+ => true
30
+
31
+ # Works as above
32
+ >> method1 [TAB]
33
+ args autocomplete some to
34
+ >> method1 'a[TAB]
35
+ args autocomplete
36
+ >> method1 'au[TAB]
37
+ >> method1 'autocomplete'
38
+
39
+ # Anything not matched by the above completion missions defaults to irb completion
40
+ >> $std[TAB]
41
+ $stderr $stdin $stdout
42
+
43
+ === File Argument Completion for Methods
44
+
45
+ # Pass :search=>false to turn off Bond searching since FILENAME_COMPLETION_PROC does it for us.
46
+ >> Bond.complete(:method=>"File.read", :search=>false) {|input|
47
+ Readline::FILENAME_COMPLETION_PROC.call(input) || [] }
48
+ => true
49
+
50
+ # Test drive it
51
+ >> File.read '[TAB]
52
+ .git/ LICENSE.txt README.rdoc Rakefile VERSION.yml bond.gemspec ext/ lib/ test/
53
+ >> File.read 'l[TAB]
54
+ >> File.read 'lib/
55
+ >> File.read 'lib/bond.[TAB]
56
+ >> File.read 'lib/bond.rb'
57
+
58
+ # Since File.read doesn't understand ~, let's improve the above completion proc
59
+ >> file_completion = proc {|input| (Readline::FILENAME_COMPLETION_PROC.call(input) ||
60
+ []).map {|f| f =~ /^~/ ? File.expand_path(f) : f } }
61
+ => #< Proc:0x0100f1d0@(irb):20>
62
+ >> Bond.reset; Bond.complete :method=>"File.read", :search=>false, &file_completion
63
+ => true
64
+
65
+ # Tilda test driving
66
+ >> File.read "~/[TAB]
67
+ >> File.read "/Users/bozo/
68
+ >> File.read "/Users/bozo/.alias.[TAB]
69
+ >> File.read "/Users/bozo/.alias.yml"
70
+
71
+ # Let's add this to *all* our File methods:
72
+ >> Bond.reset; Bond.complete :method=>/File\.(#{Regexp.union(*File.methods(false))})/,
73
+ :search=>false, &file_completion
74
+ => true
75
+
76
+ === Method Autocompletion on Specified Objects
77
+ # Let's explore Bond::Agent's functionality
78
+ >> ba = Bond.agent; nil
79
+ => nil
80
+ # Irb let's you autocomplete everything that an object responds to for better or worse.
81
+ >> ba.[TAB]
82
+ ba.__id__ ba.eql? ba.instance_eval ba.method ba.send ba.to_yaml
83
+ ba.__send__ ba.equal? ba.instance_of? ba.methods ba.setup ba.to_yaml_properties
84
+ ba.call ba.extend ba.instance_variable_defined? ba.missions ba.singleton_methods ba.to_yaml_style
85
+ ba.class ba.find_mission ba.instance_variable_get ba.nil? ba.taguri ba.type
86
+ ba.clone ba.freeze ba.instance_variable_set ba.object_id ba.taguri= ba.untaint
87
+ ba.complete ba.frozen? ba.instance_variables ba.private_methods ba.taint
88
+ ba.default_mission ba.hash ba.is_a? ba.protected_methods ba.tainted?
89
+ ba.display ba.id ba.kind_of? ba.public_methods ba.to_a
90
+ ba.dup ba.inspect ba.line_buffer ba.respond_to? ba.to_s
91
+
92
+
93
+ # Since it's hard to see Bond::Agent's functionality amidst all the Object and Kernel methods,
94
+ # let's autocomplete just it's instance methods.
95
+ >> Bond.complete(:object=>Bond::Agent) {|input| input.object.class.instance_methods(false) }
96
+ => true
97
+
98
+ # A less cluttered display of Bond::Agent's functionality.
99
+ >> ba.[TAB]
100
+ ba.call ba.complete ba.default_mission ba.find_mission ba.missions
101
+
102
+ # Let's have all Bond::* objects do this.
103
+ >> Bond.reset; Bond.complete(:object=>/^Bond::/) {|input| input.object.class.instance_methods(false) }
104
+ => true
105
+
106
+ # Let's revert method autocompletion back to irb's defaults for Bond::* objects.
107
+ >> Bond.reset; Bond.complete :object=>/^Bond::/
108
+
109
+ === Underscore Search
110
+
111
+ # Firing up a rails console
112
+ bash> script/console
113
+ >> require 'bond'
114
+ => true
115
+
116
+ # Set all ActiveRecord::Base descendants to use the predefined underscore search
117
+ >> Bond.complete :object=>ActiveRecord::Base, :search=>:underscore
118
+ => true
119
+
120
+ # With this search we can still autocomplete the traditional way.
121
+ # Url is a model object
122
+ >> Url.first.tag_[TAB]
123
+ Url.first.tag_add_and_remove Url.first.tag_and_save Url.first.tag_ids= Url.first.tag_list=
124
+ Url.first.tag_add_and_save Url.first.tag_ids Url.first.tag_list Url.first.tag_remove_and_save
125
+ >> Url.tag_ad[TAB]
126
+ >> Url.tag_add_and_
127
+ >> Url.tag_add_and_[TAB]
128
+ Url.first.tag_add_and_remove Url.first.tag_add_and_save
129
+ >> Url.tag_add_and_s[TAB]
130
+ >> Url.tag_add_and_save
131
+
132
+ # But this search goes the extra mile with textmate-like searching.
133
+ # Type just the first letter of each underscored word separated by '-'
134
+ >> Url.first.t-a-a-s[TAB]
135
+ >> Url.first.tag_add_and_save
136
+
137
+ # With this search, most multi-worded methods are just a few keystrokes away.
138
+ # If multiple methods match the underscore alias, it still autocompletes the beginning of the method:
139
+ >> Url.first.p[TAB]
140
+ Url.first.partial_updates Url.first.pretty_inspect Url.first.pretty_print_instance_variables Url.first.public_methods
141
+ Url.first.partial_updates? Url.first.pretty_print Url.first.primary_key_prefix_type
142
+ Url.first.pluralize_table_names Url.first.pretty_print_cycle Url.first.private_methods
143
+ Url.first.present? Url.first.pretty_print_inspect Url.first.protected_methods
144
+ >> Url.first.p-p[TAB]
145
+ >> Url.first.pretty_print
146
+ >> Url.first.pretty_print_c[TAB]
147
+ >> Url.first.pretty_print_cycle
148
+
149
+
150
+ === Custom AutoCompletion
151
+
152
+ bash> irb -rirb/completion -rubygems -rbond
153
+ # Let's reuse the file completion from above
154
+ >> file_completion = proc {|input| (Readline::FILENAME_COMPLETION_PROC.call(input) ||
155
+ []).map {|f| f =~ /^~/ ? File.expand_path(f) : f } }
156
+ => #< Proc:0x0100f1d0@(irb):1>
157
+
158
+ # But this time let's trigger it whenever the last word in the line is quoted
159
+ # fyi this is default behavior if you use irb without requiring irb/completion
160
+ >> Bond.complete(:on=>/\S+\s*["']([^'".]*)$/, :search=>false) {|input| file_completion.call(input.matched[1]) }
161
+ => true
162
+
163
+ # Now it doesn't matter what methods come before. If the last word is quoted we get file completion:
164
+ >> Dir.entries '[TAB]
165
+ .git/ LICENSE.txt README.rdoc Rakefile VERSION.yml bond.gemspec ext/ lib/ test/
166
+ >> Dir.entries 'l[TAB]
167
+ >> Dir.entries 'lib/
168
+ >> `ls 't[TAB]
169
+ >> `ls 'test/
170
+ >> `ls 'test/'`
171
+
172
+ # String method completion still works
173
+ >> '007'.[TAB]
174
+ Display all 137 possibilities? (y or n)
175
+
176
+ == Credits
177
+ Thanks to Csaba Hank for {providing the C extension}[http://www.creo.hu/~csaba/ruby/irb-enhancements/doc/files/README.html]
178
+ which Bond uses to read Readline's full buffer. Thanks also goes out to Takao Kouji for {recently
179
+ commiting}[http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/ext/readline/readline.c?view=diff&r1=24018&r2=24019]
180
+ this Readline enhancement to ruby.
181
+
182
+ == Bugs
183
+ The underscore search intermittently doesn't complete when it should and deletes the last
184
+ character typed.
185
+
186
+ == Todo
187
+ * Allow Bond to easily get its completion missions from a module.
188
+ * Provide a set of Bond completions which can serve as a drop-in replacement for irb/completion.
189
+ * Bond.spy for easily viewing/debugging what completion mission matches a given input.
190
+ * Argument autocompletion by object class.
191
+ * Make replacement of existing completions not require doing a Bond.reset.
@@ -0,0 +1,77 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ begin
5
+ require 'rcov/rcovtask'
6
+
7
+ Rcov::RcovTask.new do |t|
8
+ t.libs << 'test'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ t.rcov_opts = ["-T -x '/Library/Ruby/*'"]
11
+ t.verbose = true
12
+ end
13
+ rescue LoadError
14
+ puts "Rcov not available. Install it for rcov-related tasks with: sudo gem install rcov"
15
+ end
16
+
17
+ begin
18
+ require 'jeweler'
19
+ Jeweler::Tasks.new do |s|
20
+ s.name = "bond"
21
+ s.summary = "Mission: Easy custom autocompletion for arguments, methods and beyond. Accomplished for irb and any other readline-like console environments."
22
+ s.description = "Bond is on a mission to make custom autocompletion easy in irb and other console/readline-like environments. Bond supports custom argument completion of methods, method completion of objects and anything else your wicked regex's can do. Bond comes armed with a Readline C extension to get the full line of input as opposed to irb's last-word based completion. Bond makes custom searching of possible completions easy which allows for nontraditional ways of autocompleting i.e. instant aliasing of multi worded methods."
23
+ s.email = "gabriel.horner@gmail.com"
24
+ s.homepage = "http://tagaholic.me/bond/"
25
+ s.authors = ["Gabriel Horner"]
26
+ s.rubyforge_project = 'tagaholic'
27
+ s.has_rdoc = true
28
+ s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"]
29
+ s.extensions = ["ext/readline_line_buffer/extconf.rb"]
30
+ s.files = FileList["CHANGELOG.rdoc", "Rakefile", "README.rdoc", "VERSION.yml", "LICENSE.txt", "{bin,lib,test,ext}/**/*"]
31
+ end
32
+
33
+ rescue LoadError
34
+ puts "Jeweler not available. Install it for jeweler-related tasks with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
35
+ end
36
+
37
+ Rake::TestTask.new do |t|
38
+ t.libs << 'lib'
39
+ t.pattern = 'test/**/*_test.rb'
40
+ t.verbose = false
41
+ end
42
+
43
+ Rake::RDocTask.new do |rdoc|
44
+ rdoc.rdoc_dir = 'rdoc'
45
+ rdoc.title = 'test'
46
+ rdoc.options << '--line-numbers' << '--inline-source'
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
50
+
51
+ # I assume contributors can edit the gemspec for most attributes except for file-related ones.
52
+ # For those attributes you can use the gemspec_update task. I prefer not to give you the gem-creator-specific rake
53
+ # tasks I use to generate gemspecs to give you the choice of generating gemspecs however you'd like.
54
+ # More about this here: http://tagaholic.me/2009/04/08/building-dry-gems-with-thor-and-jeweler.html .
55
+ desc "Update gemspec from existing one by regenerating path globs specified in *.gemspec.yml or defaults to liberal path globs."
56
+ task :gemspec_update do
57
+ if (gemspec_file = Dir['*.gemspec'][0])
58
+ original_gemspec = eval(File.read(gemspec_file))
59
+ if File.exists?("#{gemspec_file}.yml")
60
+ require 'yaml'
61
+ YAML::load_file("#{gemspec_file}.yml").each do |attribute, globs|
62
+ original_gemspec.send("#{attribute}=", FileList[globs])
63
+ end
64
+ else
65
+ # liberal defaults
66
+ original_gemspec.files = FileList["**/*"]
67
+ test_directories = original_gemspec.test_files.grep(/\//).map {|e| e[/^[^\/]+/]}.compact.uniq
68
+ original_gemspec.test_files = FileList["{#{test_directories.join(',')}}/**/*"] unless test_directories.empty?
69
+ end
70
+ File.open(gemspec_file, 'w') {|f| f.write(original_gemspec.to_ruby) }
71
+ puts "Updated gemspec."
72
+ else
73
+ puts "No existing gemspec file found."
74
+ end
75
+ end
76
+
77
+ task :default => :test
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,3 @@
1
+ require "mkmf"
2
+ dir_config 'readline_line_buffer'
3
+ create_makefile 'readline_line_buffer'
@@ -0,0 +1,22 @@
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
+ #include "ruby.h"
6
+ #include <errno.h>
7
+ #include <stdio.h>
8
+ #include <readline/readline.h>
9
+
10
+ static VALUE line_buffer(VALUE self)
11
+ {
12
+ rb_secure(4);
13
+ if (rl_line_buffer == NULL)
14
+ return Qnil;
15
+ return rb_tainted_str_new2(rl_line_buffer);
16
+ }
17
+
18
+ void Init_readline_line_buffer() {
19
+ VALUE c = rb_cObject;
20
+ c = rb_const_get(c, rb_intern("Readline"));
21
+ rb_define_singleton_method(c, "line_buffer", (VALUE(*)(ANYARGS))line_buffer, -1);
22
+ }
@@ -0,0 +1,92 @@
1
+ current_dir = File.dirname(__FILE__)
2
+ $:.unshift(current_dir) unless $:.include?(current_dir) || $:.include?(File.expand_path(current_dir))
3
+ require 'bond/readline'
4
+ require 'bond/rawline'
5
+ require 'bond/agent'
6
+ require 'bond/search'
7
+ require 'bond/mission'
8
+ require 'bond/missions/default_mission'
9
+ require 'bond/missions/method_mission'
10
+ require 'bond/missions/object_mission'
11
+
12
+ # Bond allows easy handling and creation of completion missions/rules with Bond.complete. When Bond is asked to autocomplete, Bond looks
13
+ # up the completion missions in the order they were defined and picks the first one that matches what the user has typed.
14
+ # Bond::Agent handles finding and executing the correct completion mission.
15
+ # Some pointers on using/understanding Bond:
16
+ # * Bond can work outside of irb and readline when debriefed with Bond.debrief. This should be called before any Bond.complete calls.
17
+ # * Bond doesn't take over completion until an explicit Bond.complete is called.
18
+ # * Order of completion missions matters. The order they're defined in is the order Bond searches
19
+ # when looking for a matching completion. This means that you should probably declare general
20
+ # completions at the end.
21
+ # * If no completion missions match, then Bond falls back on a default mission. If using irb and irb/completion
22
+ # this falls back on irb's completion. Otherwise an empty completion list is returned.
23
+ module Bond
24
+ extend self
25
+
26
+ # Defines a completion mission aka a Bond::Mission. A valid mission consists of a condition and an action block.
27
+ # A condition is specified with one of the following options: :on, :object or :method. Depending on the condition option, a
28
+ # different type of Bond::Mission is created. Action blocks are given what the user has typed and should a return a list of possible
29
+ # completions. By default Bond searches possible completions to only return the ones that match what has been typed. This searching
30
+ # behavior can be customized with the :search option.
31
+ # ====Options:
32
+ # [:on] Matches the given regular expression with the full line of input. Creates a Bond::Mission object.
33
+ # Access to the matches in the regular expression are passed to the completion proc as the input's attribute :matched.
34
+ # [:method] Matches the given string or regular expression with any methods (or any non-whitespace string) that start the beginning
35
+ # of a line. Creates a Bond::Missions:MethodMission object. If given a string, the match has to be exact.
36
+ # Since this is used mainly for argument completion, completions can have an optional quote in front of them.
37
+ # [:object] Matches the given a string or regular expression to the ancestor of the current object being completed. Creates a
38
+ # Bond::Missions::ObjectMission object. Access to the current object is passed to the completion proc as the input's
39
+ # attribute :object. If no action is given, this completion type defaults to all methods the object responds to.
40
+ # [:search] Given a symbol, proc or false, determines how completions are searched to match what the user has typed. Defaults to
41
+ # traditional searching i.e. looking at the beginning of a string for possible matches. If false, search is turned off and
42
+ # assumed to be done in the action block. Possible symbols are :anywhere, :ignore_case and :underscore. See Bond::Search for
43
+ # more info about them. A proc is given two arguments: the input string and an array of possible completions.
44
+ #
45
+ # ==== Examples:
46
+ # Bond.complete(:method=>'shoot') {|input| %w{to kill} }
47
+ # Bond.complete(:on=>/^((([a-z][^:.\(]*)+):)+/, :search=>false) {|input| Object.constants.grep(/#{input.matched[1]}/) }
48
+ # Bond.complete(:object=>ActiveRecord::Base, :search=>:underscore)
49
+ # Bond.complete(:object=>ActiveRecord::Base) {|input| input.object.class.instance_methods(false) }
50
+ # Bond.complete(:method=>'you', :search=>proc {|input, list| list.grep(/#{input}/i)} ) {|input| %w{Only Live Twice} }
51
+ def complete(options={}, &block)
52
+ agent.complete(options, &block)
53
+ true
54
+ rescue InvalidMissionError
55
+ $stderr.puts "Invalid mission given. Mission needs an action and a condition."
56
+ false
57
+ rescue
58
+ $stderr.puts "Mission setup failed with:", $!
59
+ false
60
+ end
61
+
62
+ # Resets Bond so that next time Bond.complete is called, a new set of completion missions are created. This does not
63
+ # change current completion behavior.
64
+ def reset
65
+ @agent = nil
66
+ end
67
+
68
+ # Debriefs Bond to set global defaults.
69
+ # ==== Options:
70
+ # [:readline_plugin] Specifies a Bond plugin to interface with a Readline-like library. Available plugins are Bond::Readline and Bond::Rawline.
71
+ # Defaults to Bond::Readline. Note that a plugin doesn't imply use with irb. Irb is joined to the hip with Readline.
72
+ # [:default_mission] A proc to be used as the default completion proc when no completions match or one fails. When in irb with completion
73
+ # enabled, uses irb completion. Otherwise defaults to a proc with an empty completion list.
74
+ # [:eval_binding] Specifies a binding to be used with Bond::Missions::ObjectMission. When in irb, defaults to irb's main binding. Otherwise
75
+ # defaults to TOPLEVEL_BINDING.
76
+ # [:debug] Boolean to print unexpected errors when autocompletion fails. Default is false.
77
+ def debrief(options={})
78
+ config.merge! options
79
+ plugin_methods = %w{setup line_buffer}
80
+ unless config[:readline_plugin].is_a?(Module) && plugin_methods.all? {|e| config[:readline_plugin].instance_methods.include?(e)}
81
+ $stderr.puts "Invalid readline plugin set. Try again."
82
+ end
83
+ end
84
+
85
+ def agent #:nodoc:
86
+ @agent ||= Agent.new(config)
87
+ end
88
+
89
+ def config #:nodoc:
90
+ @config ||= {:readline_plugin=>Bond::Readline, :debug=>false}
91
+ end
92
+ end
@@ -0,0 +1,44 @@
1
+ module Bond
2
+ # Handles finding and executing the first mission that matches the input line. Once found, it calls the mission's action.
3
+ class Agent
4
+ # The array of missions that will be searched when a completion occurs.
5
+ attr_reader :missions
6
+
7
+ def initialize(options={}) #:nodoc:
8
+ raise ArgumentError unless options[:readline_plugin].is_a?(Module)
9
+ extend(options[:readline_plugin])
10
+ @default_mission_action = options[:default_mission] if options[:default_mission]
11
+ @eval_binding = options[:eval_binding] if options[:eval_binding]
12
+ setup
13
+ @missions = []
14
+ end
15
+
16
+ def complete(options={}, &block) #:nodoc:
17
+ @missions << Mission.create(options.merge(:action=>block, :eval_binding=>@eval_binding))
18
+ end
19
+
20
+ # This is where the action starts when a completion is initiated.
21
+ def call(input)
22
+ (mission = find_mission(input)) ? mission.execute : default_mission.execute(input)
23
+ rescue FailedExecutionError
24
+ $stderr.puts "", $!.message
25
+ rescue
26
+ if Bond.config[:debug]
27
+ p $!
28
+ p $!.backtrace.slice(0,5)
29
+ end
30
+ default_mission.execute(input)
31
+ end
32
+
33
+ # No need to use what's passed to the completion proc when we can get the full line.
34
+ def find_mission(input) #:nodoc:
35
+ all_input = line_buffer
36
+ @missions.find {|mission| mission.matches?(all_input) }
37
+ end
38
+
39
+ # Default mission used by agent.
40
+ def default_mission
41
+ @default_mission ||= Missions::DefaultMission.new(:action=>@default_mission_action)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ module Bond
2
+ # Occurs when a mission is incorrectly defined.
3
+ class InvalidMissionError < StandardError; end
4
+ # Occurs when a mission or search action fails.
5
+ class FailedExecutionError < StandardError; end
6
+ # Namespace for subclasses of Bond::Mission.
7
+ class Missions; end
8
+
9
+ # A set of conditions and actions to take for a completion scenario or mission in Bond's mind.
10
+ class Mission
11
+ include Search
12
+
13
+ # Handles creation of proper Mission class depending on the options passed.
14
+ def self.create(options)
15
+ if options[:method]
16
+ Missions::MethodMission.new(options)
17
+ elsif options[:object]
18
+ Missions::ObjectMission.new(options)
19
+ else
20
+ new(options)
21
+ end
22
+ end
23
+
24
+ attr_reader :action
25
+ OPERATORS = ["%", "&", "*", "**", "+", "-", "/", "<", "<<", "<=", "<=>", "==", "===", "=~", ">", ">=", ">>", "[]", "[]=", "^"]
26
+
27
+ # Options are almost the same as those explained at Bond.complete. The only difference is that the action is passed
28
+ # as an :action option here.
29
+ def initialize(options)
30
+ raise InvalidMissionError unless (options[:action] || respond_to?(:default_action)) &&
31
+ (options[:on] || is_a?(Missions::DefaultMission))
32
+ raise InvalidMissionError if options[:on] && !options[:on].is_a?(Regexp)
33
+ @action = options[:action]
34
+ @condition = options[:on]
35
+ @search = options.has_key?(:search) ? options[:search] : method(:default_search)
36
+ @search = method("#{options[:search]}_search") if respond_to?("#{options[:search]}_search")
37
+ end
38
+
39
+ # Returns a boolean indicating if a mission matches the given input.
40
+ def matches?(input)
41
+ if (match = handle_valid_match(input))
42
+ @input.instance_variable_set("@matched", @matched)
43
+ @input.instance_eval("def self.matched; @matched ; end")
44
+ end
45
+ !!match
46
+ end
47
+
48
+ # Called when a mission has been chosen to autocomplete.
49
+ def execute(*args)
50
+ if args.empty?
51
+ list = @action.call(@input) || []
52
+ list = @search ? @search.call(@input, list) : list
53
+ @list_prefix ? list.map {|e| @list_prefix + e } : list
54
+ else
55
+ @action.call(*args)
56
+ end
57
+ rescue
58
+ error_message = "Mission action failed to execute properly. Check your mission action with pattern #{@condition.inspect}.\n" +
59
+ "Failed with error: #{$!.message}"
60
+ raise FailedExecutionError, error_message
61
+ end
62
+
63
+ #:stopdoc:
64
+ def set_input(input, match)
65
+ @input = input[/\S+$/]
66
+ end
67
+
68
+ def handle_valid_match(input)
69
+ if (match = input.match(@condition))
70
+ set_input(input, match)
71
+ @matched ||= match
72
+ end
73
+ match
74
+ end
75
+ #:startdoc:
76
+ end
77
+ end
@@ -0,0 +1,11 @@
1
+ # Represents a default mission which doesn't need an explicit action.
2
+ class Bond::Missions::DefaultMission < Bond::Mission
3
+ def initialize(options={}) #:nodoc:
4
+ options[:action] ||= default_action
5
+ super
6
+ end
7
+
8
+ def default_action #:nodoc:
9
+ Object.const_defined?(:IRB) && IRB.const_defined?(:InputCompletor) ? IRB::InputCompletor::CompletionProc : lambda {|e| [] }
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # Represents a completion mission specified by :method in Bond.complete.
2
+ class Bond::Missions::MethodMission < Bond::Mission
3
+ def initialize(options={}) #:nodoc:
4
+ @method = options.delete(:method)
5
+ @method = Regexp.quote(@method.to_s) unless @method.is_a?(Regexp)
6
+ options[:on] = /^\s*(#{@method})\s*['"]?(.*)$/
7
+ super
8
+ end
9
+
10
+ def set_input(input, match) #:nodoc:
11
+ @input = match[-1]
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ # Represents a completion mission specified by :object in Bond.complete. Unlike other missions, this
2
+ # one needs to both match the mission condition and have the current object being completed have
3
+ # an ancestor specified by :object.
4
+ class Bond::Missions::ObjectMission < Bond::Mission
5
+ #:stopdoc:
6
+ def initialize(options={})
7
+ @object_condition = options.delete(:object)
8
+ @object_condition = /^#{Regexp.quote(@object_condition.to_s)}$/ unless @object_condition.is_a?(Regexp)
9
+ options[:on] = /^((\.?[^.]+)+)\.([^.]*)$/
10
+ @eval_binding = options[:eval_binding] || default_eval_binding
11
+ super
12
+ end
13
+
14
+ def handle_valid_match(input)
15
+ match = super
16
+ if match && eval_object(match) && (match = @evaled_object.class.ancestors.any? {|e| e.to_s =~ @object_condition })
17
+ @list_prefix = @matched[1] + "."
18
+ @input = @matched[3]
19
+ @input.instance_variable_set("@object", @evaled_object)
20
+ @input.instance_eval("def self.object; @object ; end")
21
+ @action ||= lambda {|e| default_action(e.object) }
22
+ else
23
+ match = false
24
+ end
25
+ match
26
+ end
27
+
28
+ def eval_object(match)
29
+ @matched = match
30
+ @evaled_object = begin eval("#{match[1]}", @eval_binding); rescue Exception; nil end
31
+ end
32
+
33
+ def default_action(obj)
34
+ obj.methods - OPERATORS
35
+ end
36
+
37
+ def default_eval_binding
38
+ Object.const_defined?(:IRB) ? IRB.CurrentContext.workspace.binding : ::TOPLEVEL_BINDING
39
+ end
40
+ #:startdoc:
41
+ end
@@ -0,0 +1,16 @@
1
+ module Bond
2
+ # A readline plugin for use with {Rawline}[http://github.com/h3rald/rawline/tree/master]. This plugin
3
+ # should be used in conjunction with {a Rawline shell}[http://www.h3rald.com/articles/real-world-rawline-usage].
4
+ module Rawline
5
+ def setup
6
+ require 'rawline'
7
+ ::Rawline.completion_append_character = nil
8
+ ::Rawline.basic_word_break_characters= " \t\n\"\\'`><;|&{("
9
+ ::Rawline.completion_proc = self
10
+ end
11
+
12
+ def line_buffer
13
+ ::Rawline.editor.line.text
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,47 @@
1
+
2
+ module Bond
3
+ # This is the default readline plugin for Bond. A valid plugin must define methods setup() and line_buffer(). setup()
4
+ # should load the readline-like library and set the completion_proc. line_buffer() should give access to the full line of what
5
+ # the user has typed.
6
+ module Readline
7
+ DefaultBreakCharacters = " \t\n\"\\'`><=;|&{("
8
+
9
+ def setup
10
+ require 'readline'
11
+ begin
12
+ require 'readline_line_buffer'
13
+ rescue LoadError
14
+ $stderr.puts "Failed to load readline_line_buffer extension. Falling back on RubyInline extension."
15
+ require 'inline'
16
+ eval %[
17
+ module ::Readline
18
+ inline do |builder|
19
+ %w(<errno.h> <stdio.h> <readline/readline.h>).each{|h| builder.include h }
20
+ builder.c_raw_singleton <<-EOC
21
+ static VALUE line_buffer(VALUE self)
22
+ {
23
+ rb_secure(4);
24
+ if (rl_line_buffer == NULL)
25
+ return Qnil;
26
+ return rb_tainted_str_new2(rl_line_buffer);
27
+ }
28
+ EOC
29
+ end
30
+ end
31
+ ]
32
+ end
33
+
34
+ # Reinforcing irb defaults
35
+ ::Readline.completion_append_character = nil
36
+ if ::Readline.respond_to?("basic_word_break_characters=")
37
+ ::Readline.basic_word_break_characters = DefaultBreakCharacters
38
+ end
39
+
40
+ ::Readline.completion_proc = self
41
+ end
42
+
43
+ def line_buffer
44
+ ::Readline.line_buffer
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ module Bond
2
+ # Contains search methods used to filter possible completions given what the user has typed for that completion.
3
+ module Search
4
+ # Searches completions from the beginning of the string.
5
+ def default_search(input, list)
6
+ list.grep(/^#{input}/)
7
+ end
8
+
9
+ # Searches completions anywhere in the string.
10
+ def anywhere_search(input, list)
11
+ list.grep(/#{input}/)
12
+ end
13
+
14
+ # Searches completions from the beginning and ignores case.
15
+ def ignore_case_search(input, list)
16
+ list.grep(/^#{input}/i)
17
+ end
18
+
19
+ # Searches completions from the beginning but also provides aliasing of underscored words.
20
+ # For example 'some_dang_long_word' can be specified as 's-d-l-w'. Aliases can be any unique string
21
+ # at the beginning of an underscored word. For example, to choose the first completion between 'so_long' and 'so_larger',
22
+ # type 's-lo'.
23
+ def underscore_search(input, list)
24
+ split_input = input.split("-").join("")
25
+ list.select {|c|
26
+ c.split("_").map {|g| g[0,1] }.join("") =~ /^#{split_input}/ || c =~ /^#{input}/
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Bond::AgentTest < Test::Unit::TestCase
4
+ before(:all) {|e| Bond.debrief(:readline_plugin=>valid_readline_plugin) }
5
+
6
+ context "InvalidAgent" do
7
+ test "prints error if no action given for mission" do
8
+ capture_stderr { Bond.complete :on=>/blah/ }.should =~ /Invalid mission/
9
+ end
10
+
11
+ test "prints error if no condition given" do
12
+ capture_stderr { Bond.complete {|e| []} }.should =~ /Invalid mission/
13
+ end
14
+
15
+ test "prints error if invalid condition given" do
16
+ capture_stderr { Bond.complete(:on=>'blah') {|e| []} }.should =~ /Invalid mission/
17
+ end
18
+
19
+ test "prints error if setting mission fails unpredictably" do
20
+ Bond.agent.expects(:complete).raises(ArgumentError)
21
+ capture_stderr { Bond.complete(:on=>/blah/) {|e| [] } }.should =~ /Mission setup failed/
22
+ end
23
+ end
24
+
25
+ context "Agent" do
26
+ before(:each) {|e| Bond.agent.instance_eval("@missions = []") }
27
+
28
+ test "chooses default mission if no missions match" do
29
+ Bond.complete(:on=>/bling/) {|e| [] }
30
+ Bond.agent.default_mission.expects(:execute)
31
+ complete 'blah'
32
+ end
33
+
34
+ test "chooses default mission if internal processing fails" do
35
+ Bond.complete(:on=>/bling/) {|e| [] }
36
+ Bond.agent.expects(:find_mission).raises
37
+ Bond.agent.default_mission.expects(:execute)
38
+ complete('bling')
39
+ end
40
+
41
+ test "prints error if action generates failure" do
42
+ Bond.complete(:on=>/bling/) {|e| raise "whoops" }
43
+ capture_stderr { complete('bling') }.should =~ /bling.*whoops/m
44
+ end
45
+ end
46
+
47
+ # TODO
48
+ # test "default_mission set to a valid mission if irb doesn't exist" do
49
+ # old_default_action = Bond.agent.default_mission.action
50
+ # Bond.agent.instance_eval("@default_mission = @default_mission_action = nil")
51
+ # Object.expects(:const_defined?).with(:IRB).returns(false)
52
+ # Bond.agent.default_mission.is_a?(Bond::Mission).should == true
53
+ # Bond.agent.default_mission.action.respond_to?(:call).should == true
54
+ # Bond.agent.default_mission.instance_variable_set(:@action, old_default_action)
55
+ # end
56
+
57
+ # test "binding set to toplevel binding if irb doesn't exist" do
58
+ # old_binding = Bond.agent.eval_binding
59
+ # Object.expects(:const_defined?).with(:IRB).returns(false)
60
+ # Bond.agent.instance_eval("@eval_binding = nil")
61
+ # Bond.agent.eval_binding.should == ::TOPLEVEL_BINDING
62
+ # Bond.agent.instance_variable_set(:@eval_binding, old_binding)
63
+ # end
64
+ end
@@ -0,0 +1,31 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class BondTest < Test::Unit::TestCase
4
+ context "debrief" do
5
+ before(:each) {|e| Bond.instance_eval("@agent = @config = nil")}
6
+ test "prints error if readline_plugin is not a module" do
7
+ capture_stderr { Bond.debrief :readline_plugin=>false }.should =~ /Invalid/
8
+ end
9
+
10
+ test "prints error if readline_plugin doesn't have all required methods" do
11
+ capture_stderr {Bond.debrief :readline_plugin=>Module.new{ def setup; end } }.should =~ /Invalid/
12
+ end
13
+
14
+ test "no error if valid readline_plugin" do
15
+ capture_stderr {Bond.debrief :readline_plugin=>valid_readline_plugin }.should == ''
16
+ end
17
+
18
+ test "sets default mission" do
19
+ default_mission = lambda {}
20
+ Bond.debrief :default_mission=>default_mission, :readline_plugin=>valid_readline_plugin
21
+ Bond.agent.default_mission.action.should == default_mission
22
+ end
23
+ end
24
+
25
+ test "reset clears existing missions" do
26
+ Bond.complete(:on=>/blah/) {[]}
27
+ Bond.agent.missions.size.should_not == 0
28
+ Bond.reset
29
+ Bond.agent.missions.size.should == 0
30
+ end
31
+ end
@@ -0,0 +1,87 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Bond::MissionTest < Test::Unit::TestCase
4
+ before(:all) {|e| Bond.debrief(:readline_plugin=>valid_readline_plugin) }
5
+
6
+ context "mission" do
7
+ before(:each) {|e| Bond.agent.instance_eval("@missions = []") }
8
+ test "completes" do
9
+ Bond.complete(:on=>/bling/) {|e| %w{ab cd fg hi}}
10
+ Bond.complete(:method=>'cool') {|e| [] }
11
+ complete('some bling f', 'f').should == %w{fg}
12
+ end
13
+
14
+ test "with method completes" do
15
+ Bond.complete(:on=>/bling/) {|e| [] }
16
+ Bond.complete(:method=>'cool') {|e| %w{ab cd ef gd} }
17
+ complete('cool c', 'c').should == %w{cd}
18
+ end
19
+
20
+ test "with method and quoted argument completes" do
21
+ Bond.complete(:on=>/bling/) {|e| [] }
22
+ Bond.complete(:method=>'cool') {|e| %w{ab cd ef ad} }
23
+ complete('cool "a', 'a').should == %w{ab ad}
24
+ end
25
+
26
+ test "with string method completes exact matches" do
27
+ Bond.complete(:method=>'cool?') {|e| [] }
28
+ Bond.complete(:method=>'cool') {|e| %w{ab cd ef gd} }
29
+ complete('cool c', 'c').should == %w{cd}
30
+ end
31
+
32
+ test "with regex method completes multiple methods" do
33
+ Bond.complete(:method=>/cool|ls/) {|e| %w{ab cd ef ad}}
34
+ complete("cool a").should == %w{ab ad}
35
+ complete("ls c").should == %w{cd}
36
+ end
37
+
38
+ test "with no search option and matching completes" do
39
+ Bond.complete(:on=>/\s*'([^']+)$/, :search=>false) {|e| %w{coco for puffs}.grep(/#{e.matched[1]}/) }
40
+ complete("require 'ff").should == ['puffs']
41
+ end
42
+
43
+ test "with search proc completes" do
44
+ Bond.complete(:method=>'blah', :search=>proc {|input, list| list.grep(/#{input}/)}) {|e| %w{coco for puffs} }
45
+ complete("blah 'ff").should == ['puffs']
46
+ end
47
+
48
+ test "with anywhere search completes" do
49
+ Bond.complete(:method=>'blah', :search=>:anywhere) {|e| %w{coco for puffs} }
50
+ complete("blah 'ff").should == ['puffs']
51
+ end
52
+
53
+ test "with ignore case search completes" do
54
+ Bond.complete(:method=>'blah', :search=>:ignore_case) {|e| %w{Coco For PufFs} }
55
+ complete("blah 'pu").should == ['PufFs']
56
+ end
57
+
58
+ test "with underscore search completes" do
59
+ Bond.complete(:on=>/blah/, :search=>:underscore) {|e| %w{and_one big_two can_three} }
60
+ complete("blah and").should == ['and_one']
61
+ complete("blah b-t").should == ['big_two']
62
+ end
63
+
64
+ test "with object and default action completes" do
65
+ Bond.complete(:object=>"String")
66
+ Bond.complete(:on=>/man/) { %w{upper upster upful}}
67
+ complete("'man'.u").should == ["'man'.upcase!", "'man'.unpack", "'man'.untaint", "'man'.upcase", "'man'.upto"]
68
+ end
69
+
70
+ test "with regex object completes" do
71
+ Bond.complete(:object=>/Str/) {|e| e.object.class.superclass.instance_methods(true) }
72
+ Bond.complete(:on=>/man/) { %w{upper upster upful}}
73
+ complete("'man'.u").should == ["'man'.untaint"]
74
+ end
75
+
76
+ test "with object and explicit action completes" do
77
+ Bond.complete(:object=>"String") {|e| e.object.class.superclass.instance_methods(true) }
78
+ Bond.complete(:on=>/man/) { %w{upper upster upful}}
79
+ complete("'man'.u").should == ["'man'.untaint"]
80
+ end
81
+
82
+ test "ignores invalid objects" do
83
+ Bond.complete(:object=>"String")
84
+ complete("blah.upt").should == []
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'context' #gem install jeremymcanally-context -s http://gems.github.com
4
+ require 'matchy' #gem install jeremymcanally-matchy -s http://gems.github.com
5
+ require 'mocha'
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ require 'bond'
8
+
9
+ class Test::Unit::TestCase
10
+ before(:all) {
11
+ # Mock irb
12
+ unless Object.const_defined?(:IRB)
13
+ eval %[
14
+ module ::IRB
15
+ class<<self; attr_accessor :CurrentContext; end
16
+ module InputCompletor; CompletionProc = lambda {|e| [] }; end
17
+ end
18
+ ]
19
+ ::IRB.CurrentContext = stub(:workspace=>stub(:binding=>binding))
20
+ end
21
+ }
22
+
23
+ def capture_stderr(&block)
24
+ original_stderr = $stderr
25
+ $stderr = fake = StringIO.new
26
+ begin
27
+ yield
28
+ ensure
29
+ $stderr = original_stderr
30
+ end
31
+ fake.string
32
+ end
33
+
34
+ def complete(full_line, last_word=full_line)
35
+ Bond.agent.stubs(:line_buffer).returns(full_line)
36
+ Bond.agent.call(last_word)
37
+ end
38
+
39
+ def valid_readline_plugin
40
+ Module.new{ def setup; end; def line_buffer; end }
41
+ end
42
+ end
43
+
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bond
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gabriel Horner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-16 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Bond is on a mission to make custom autocompletion easy in irb and other console/readline-like environments. Bond supports custom argument completion of methods, method completion of objects and anything else your wicked regex's can do. Bond comes armed with a Readline C extension to get the full line of input as opposed to irb's last-word based completion. Bond makes custom searching of possible completions easy which allows for nontraditional ways of autocompleting i.e. instant aliasing of multi worded methods.
17
+ email: gabriel.horner@gmail.com
18
+ executables: []
19
+
20
+ extensions:
21
+ - ext/readline_line_buffer/extconf.rb
22
+ extra_rdoc_files:
23
+ - LICENSE.txt
24
+ - README.rdoc
25
+ files:
26
+ - CHANGELOG.rdoc
27
+ - LICENSE.txt
28
+ - README.rdoc
29
+ - Rakefile
30
+ - VERSION.yml
31
+ - ext/readline_line_buffer/extconf.rb
32
+ - ext/readline_line_buffer/readline_line_buffer.c
33
+ - lib/bond.rb
34
+ - lib/bond/agent.rb
35
+ - lib/bond/mission.rb
36
+ - lib/bond/missions/default_mission.rb
37
+ - lib/bond/missions/method_mission.rb
38
+ - lib/bond/missions/object_mission.rb
39
+ - lib/bond/rawline.rb
40
+ - lib/bond/readline.rb
41
+ - lib/bond/search.rb
42
+ - test/agent_test.rb
43
+ - test/bond_test.rb
44
+ - test/mission_test.rb
45
+ - test/test_helper.rb
46
+ has_rdoc: true
47
+ homepage: http://tagaholic.me/bond/
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --charset=UTF-8
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project: tagaholic
70
+ rubygems_version: 1.3.2
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: "Mission: Easy custom autocompletion for arguments, methods and beyond. Accomplished for irb and any other readline-like console environments."
74
+ test_files:
75
+ - test/agent_test.rb
76
+ - test/bond_test.rb
77
+ - test/mission_test.rb
78
+ - test/test_helper.rb