bond 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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