ori 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ module ORI
2
+ # Propose config defaults based on OS and environment.
3
+ class AutoConfig #:nodoc:
4
+ # Value of <tt>RbConfig::Config["host_os"]</tt>.
5
+ #
6
+ # linux-gnu
7
+ # mswin32
8
+ # cygwin
9
+ attr_reader :host_os
10
+
11
+ def initialize(attrs = {})
12
+ attrs.each {|k, v| send("#{k}=", v)}
13
+ clear_cache
14
+ end
15
+
16
+ #--------------------------------------- Accessors and pseudo-accessors
17
+
18
+ def has_less?
19
+ @cache[:has_less] ||= begin
20
+ require_host_os
21
+ !!@host_os.match(/cygwin|darwin|freebsd|gnu|linux/i)
22
+ end
23
+ end
24
+
25
+ def host_os=(s)
26
+ @host_os = s
27
+ clear_cache
28
+ end
29
+
30
+ def unix?
31
+ @cache[:is_unix] ||= begin
32
+ require_host_os
33
+ !!@host_os.match(/cygwin|darwin|freebsd|gnu|linux|sunos|solaris/i)
34
+ end
35
+ end
36
+
37
+ def windows?
38
+ @cache[:is_windows] ||= begin
39
+ require_host_os
40
+ !!@host_os.match(/mswin|windows/i)
41
+ end
42
+ end
43
+
44
+ #--------------------------------------- Defaults
45
+
46
+ def color
47
+ @cache[:color] ||= unix?? true : false
48
+ end
49
+
50
+ def frontend
51
+ @cache[:frontend] ||= unix?? "ri -T -f ansi %s" : "ri -T %s"
52
+ end
53
+
54
+ def pager
55
+ @cache[:pager] ||= has_less?? "less -R" : "more"
56
+ end
57
+
58
+ def shell_escape
59
+ @cache[:shell_escape] ||= if unix?
60
+ :unix
61
+ elsif windows?
62
+ :windows
63
+ else
64
+ nil
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def clear_cache
71
+ @cache = {}
72
+ end
73
+
74
+ def require_host_os
75
+ raise "`host_os` is not set" if not @host_os
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,62 @@
1
+ module ORI
2
+ # Simplistic ANSI colorizer.
3
+ module Colorize #:nodoc:
4
+ # Issue an ANSI color sequence.
5
+ #
6
+ # puts [Colorize.seq(:message, :error), "Error!", Colorize.seq(:reset)].join
7
+ def self.seq(*spec)
8
+ Tools.ansi(*case spec
9
+ when [:choice, :title]
10
+ [:green]
11
+ when [:choice, :index]
12
+ [:yellow, :bold]
13
+ when [:choice, :label]
14
+ [:cyan]
15
+ when [:choice, :prompt]
16
+ [:yellow, :bold]
17
+
18
+ # These go in sequence, each knows who's before. Thus we minimize ANSI.
19
+ when [:list_method, :own_marker]
20
+ [:reset, :bold]
21
+ when [:list_method, :not_own_marker]
22
+ [:reset]
23
+ when [:list_method, :obj_module_name]
24
+ [:cyan, :bold]
25
+ when [:list_method, :owner_name]
26
+ [:reset]
27
+ when [:list_method, :access]
28
+ [:reset, :cyan]
29
+ when [:list_method, :name]
30
+ [:reset, :bold]
31
+ when [:list_method, :visibility]
32
+ [:reset, :yellow]
33
+
34
+ # These go in sequence.
35
+ when [:mam, :module_name]
36
+ [:cyan, :bold]
37
+ when [:mam, :access]
38
+ [:reset, :cyan]
39
+ when [:mam, :method_name]
40
+ [:reset, :bold]
41
+
42
+ when [:message, :action]
43
+ [:green]
44
+ when [:message, :error]
45
+ [:red, :bold]
46
+ when [:message, :info]
47
+ [:green]
48
+
49
+ when [:reset]
50
+ [:reset]
51
+
52
+ else
53
+ raise ArgumentError, "Unknown spec: #{spec.inspect}"
54
+ end
55
+ ) # Tools.ansi
56
+ end
57
+
58
+ def self.colorize(*args)
59
+ args.map {|v| v.is_a?(Array) ? seq(*v) : v}.join
60
+ end
61
+ end # Colorize
62
+ end
@@ -0,0 +1,27 @@
1
+ module ORI
2
+ # Configuration object.
3
+ class Config
4
+ # Enable color. Example:
5
+ #
6
+ # true
7
+ attr_accessor :color
8
+
9
+ # RI frontend command to use. <tt>%s</tt> is replaced with sought topic. Example:
10
+ #
11
+ # ri -T -f ansi %s
12
+ attr_accessor :frontend
13
+
14
+ # Paging program to use. Examples:
15
+ #
16
+ # less -R
17
+ # more
18
+ attr_accessor :pager
19
+
20
+ # Shell escape mode. <tt>:unix</tt> or <tt>:windows</tt>.
21
+ attr_accessor :shell_escape
22
+
23
+ def initialize(attrs = {})
24
+ attrs.each {|k, v| send("#{k}=", v)}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module ORI
2
+ module Extensions #:nodoc:
3
+ end
4
+ end
@@ -0,0 +1,88 @@
1
+ module ORI
2
+ module Extensions
3
+ module Object
4
+ # View RI pages on module, class, method. Interactively list receiver's methods.
5
+ #
6
+ # == Request RI on a Class
7
+ #
8
+ # Array.ri
9
+ # String.ri
10
+ # [].ri
11
+ # "".ri
12
+ # 5.ri
13
+ #
14
+ # So that's fairly straightforward -- grab a class or class instance and call <tt>ri</tt> on it:
15
+ #
16
+ # obj = SomeKlass.new
17
+ # obj.ri
18
+ #
19
+ # == Request RI on a Method
20
+ #
21
+ # String.ri :upcase
22
+ # "".ri :upcase
23
+ # [].ri :sort
24
+ # Hash.ri :[]
25
+ # Hash.ri "::[]"
26
+ # Hash.ri "#[]"
27
+ #
28
+ # == Request Interactive Method List
29
+ #
30
+ # # Regular expression argument denotes list request.
31
+ # String.ri //
32
+ # "".ri //
33
+ #
34
+ # # Show method names matching a regular expression.
35
+ # "".ri /case/
36
+ # "".ri /^to_/
37
+ # [].ri /sort/
38
+ # {}.ri /each/
39
+ #
40
+ # # Show ALL methods, including those private of Kernel.
41
+ # Hash.ri //, :all => true
42
+ # Hash.ri //, :all
43
+ #
44
+ # # Show class methods or instance methods only.
45
+ # Module.ri //, :access => "::"
46
+ # Module.ri //, :access => "#"
47
+ #
48
+ # # Show own methods only.
49
+ # Time.ri //, :own => true
50
+ # Time.ri //, :own
51
+ #
52
+ # # Specify visibility: public, protected or private.
53
+ # Module.ri //, :visibility => :private
54
+ # Module.ri //, :visibility => [:public, :protected]
55
+ #
56
+ # # Filter fully formatted name by given regexp.
57
+ # Module.ri //, :fullre => /\(Object\)::/
58
+ #
59
+ # # Combine options.
60
+ # Module.ri //, :fullre => /\(Object\)::/, :access => "::", :visibility => :private
61
+ #
62
+ # == Request Interactive Method List for More Than 1 Object at Once
63
+ #
64
+ # By using the <tt>:join</tt> option it's possible to fetch methods for more
65
+ # than 1 object at once. Value of <tt>:join</tt> (which can be an object or an array)
66
+ # is joined with the original receiver, and then a combined set is queried.
67
+ #
68
+ # # List all division-related methods from numeric classes.
69
+ # Fixnum.ri /div/, :join => [Float, Rational]
70
+ # 5.ri /div/, :join => [5.0, 5.to_r]
71
+ #
72
+ # # List all ActiveSupport extensions to numeric classes.
73
+ # 5.ri //, :join => [5.0, 5.to_r], :fullre => /ActiveSupport/
74
+ #
75
+ # # Query entire Rails family for methods having the word "javascript".
76
+ # rails_modules = ObjectSpace.each_object(Module).select {|mod| mod.to_s.match /Active|Action/}
77
+ # "".ri /javascript/, :join => rails_modules
78
+ def ri(*args)
79
+ ::ORI::Internals.do_history
80
+ ::ORI::Internals.ri(self, *args)
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ class Object #:nodoc:
87
+ include ::ORI::Extensions::Object
88
+ end
@@ -0,0 +1,411 @@
1
+ module ORI
2
+ # Tools used internally by ORI.
3
+ module Internals #:nodoc:
4
+ GLM_ALL_ACCESSES = ["::", "#"]
5
+ GLM_ALL_VISIBILITIES = [:public, :protected, :private]
6
+
7
+ # Error message for the user. Sometimes it's CAUSED by the user, sometimes it's influenced by him.
8
+ class UserError < Exception #:nodoc:
9
+ end
10
+
11
+ # Non-destructive break request.
12
+ class Break < Exception #:nodoc:
13
+ end
14
+
15
+ # Apply smart filters on array of <tt>ListMethod</tt>. Return filtered array.
16
+ def self.apply_smart_filters(obj, list_methods)
17
+ # Filters.
18
+ #
19
+ # * Filters return false if record is "bad". Any other return result means that record is "good".
20
+ filters = []
21
+
22
+ # Hide all methods starting with "_ori_".
23
+ filters << proc do |r|
24
+ if r.method_name.match /\A_ori_/
25
+ false
26
+ end
27
+ end
28
+
29
+ # Obj is not Kernel.
30
+ if (obj != Kernel rescue false)
31
+ filters << proc do |r|
32
+ # Chop off Kernel's non-public methods.
33
+ if r.owner == Kernel and not r.public?
34
+ false
35
+ end
36
+ end
37
+ end
38
+
39
+ # Obj is an object.
40
+ if not obj.is_a? Module
41
+ filters << proc do |r|
42
+ # Chop off non-public methods.
43
+ if not r.public?
44
+ false
45
+ end
46
+ end
47
+ end
48
+
49
+ # Obj is a module or a class.
50
+ if obj.is_a? Module
51
+ filters << proc do |r|
52
+ # Chop off others' private instance methods.
53
+ # NOTE: We shouldn't chop private singleton methods since they are callable from the context of our class. See Sample::BasicExtension::Klass.
54
+ if not r.own? and r.private? and r.instance?
55
+ false
56
+ end
57
+ end
58
+ end
59
+
60
+ # Go! If any filter rejects the record, it's rejected.
61
+ list_methods.reject do |r|
62
+ filters.any? {|f| f.call(r) == false}
63
+ end
64
+ end
65
+
66
+ # Process interactive choice.
67
+ # Return chosen item or <tt>nil</tt>.
68
+ #
69
+ # choice [
70
+ # ["wan", 1.0],
71
+ # ["tew", 2.0],
72
+ # ["free", 3.0],
73
+ # ]
74
+ #
75
+ # Options:
76
+ #
77
+ # :colorize_labels => T|F # Default is true.
78
+ # :item_indent => " " # Default is " ".
79
+ # :prompt => ">" # Default is ">>".
80
+ # :title => "my title" # Dialog title. Default is nil (no title).
81
+ #
82
+ # :on_abort => obj # Result to return on abort (Ctrl-C). Default is nil.
83
+ # :on_skip => obj # Treat empty input as skip action. Default is nil.
84
+ def self.choice(items, options = {})
85
+ raise ArgumentError, "At least 1 item required" if items.size < 1
86
+
87
+ options = options.dup
88
+ o = {}
89
+
90
+ o[k = :colorize_labels] = (v = options.delete(k)).nil?? true : v
91
+ o[k = :item_indent] = (v = options.delete(k)).nil?? " " : v
92
+ o[k = :prompt] = (v = options.delete(k)).nil?? ">>" : v
93
+ o[k = :title] = options.delete(k)
94
+
95
+ o[k = :on_abort] = options.delete(k)
96
+ o[k = :on_skip] = options.delete(k)
97
+
98
+ raise ArgumentError, "Unknown option(s): #{options.inspect}" if not options.empty?
99
+
100
+ # Convert `items` into an informative hash.
101
+ hitems = []
102
+ items.each_with_index do |item, i|
103
+ hitems << {
104
+ :index => (i + 1).to_s, # Convert to string here, which eliminates the need to do String -> Integer after input.
105
+ :label => item[0],
106
+ :value => item[1],
107
+ }
108
+ end
109
+
110
+ ### Begin dialog. ###
111
+
112
+ if not (s = o[:title].to_s).empty?
113
+ puts colorize([:choice, :title], s, [:reset])
114
+ puts
115
+ end
116
+
117
+ # Print items.
118
+ index_nchars = hitems.size.to_s.size
119
+ hitems.each do |h|
120
+ puts colorize(*[
121
+ [
122
+ o[:item_indent],
123
+ [:choice, :index], "%*d" % [index_nchars, h[:index]],
124
+ " ",
125
+ ],
126
+ (o[:colorize_labels] ? [[:choice, :label], h[:label]] : [h[:label]]),
127
+ [[:reset]],
128
+ ].flatten(1))
129
+ end
130
+ puts
131
+
132
+ # Read input.
133
+
134
+ # Catch INT for a while.
135
+ old_sigint = trap("INT") do
136
+ puts "\nAborted"
137
+ return o[:on_abort]
138
+ end
139
+
140
+ # WARNING: Return result of `while` is return result of method.
141
+ while true
142
+ print colorize([:choice, :prompt], o[:prompt], " ", [:reset])
143
+
144
+ input = gets.strip
145
+ if input.empty?
146
+ if o[:on_skip]
147
+ break o[:on_skip]
148
+ else
149
+ next
150
+ end
151
+ end
152
+
153
+ # Something has been input.
154
+ found = hitems.find {|h| h[:index] == input}
155
+ break found[:value] if found
156
+
157
+ puts colorize([:message, :error], "Invalid input", [:reset])
158
+ end # while true
159
+ ensure
160
+ # NOTE: `old_sigint` is literally declared above, so it always exists here no matter when we gain control.
161
+ if not old_sigint.nil?
162
+ trap("INT", &old_sigint)
163
+ end
164
+ end # choice
165
+
166
+ # Same as <tt>ORI::Colorize.colorize</tt>, but this one produces
167
+ # plain output if color is turned off in <tt>ORI.conf</tt>.
168
+ def self.colorize(*args)
169
+ Colorize.colorize *args.reject {|v| v.is_a? Array and not ::ORI.conf.color}
170
+ end
171
+
172
+ # Colorize a MAM (module-access-method) array.
173
+ #
174
+ # colorize_mam(["Kernel", "#", "dup"])
175
+ def self.colorize_mam(mam)
176
+ colorize(*[
177
+ [:mam, :module_name], mam[0],
178
+ [:mam, :access], mam[1],
179
+ [:mam, :method_name], mam[2],
180
+ [:reset],
181
+ ])
182
+ end
183
+
184
+ # Stuff a ready-made "<subject>.ri " command into Readline history if last request had an argument.
185
+ def self.do_history
186
+ # `cmd` is actually THIS command being executed.
187
+ cmd = Readline::HISTORY.to_a.last
188
+ if prefix = get_ri_arg_prefix(cmd)
189
+ Readline::HISTORY.pop
190
+ Readline::HISTORY.push "#{prefix} "
191
+ Readline::HISTORY.push cmd
192
+ end
193
+ end
194
+
195
+ # Fetch ListMethods from one or more objects (<tt>:obj => ...</tt>) and optionally filter them.
196
+ # Options:
197
+ #
198
+ # :access => "#" # "#" or "::".
199
+ # :all => true|false # Show all methods. Default is `false`.
200
+ # :fullre => Regexp # Full record filter.
201
+ # :objs => Array # Array of objects to fetch methods of. Must be specified.
202
+ # :own => true|false # Show own methods only.
203
+ # :re => Regexp # Method name filter.
204
+ # :visibility => :protected # Symbol or [Symbol, Symbol, ...].
205
+ def self.get_list_methods(options = {})
206
+ options = options.dup
207
+ o = {}
208
+
209
+ o[k = :access] = if v = options.delete(k); v.to_s; end
210
+ o[k = :all] = (v = options.delete(k)).nil?? false : v
211
+ o[k = :fullre] = options.delete(k)
212
+ o[k = :objs] = options.delete(k)
213
+ o[k = :own] = (v = options.delete(k)).nil?? false : v
214
+ o[k = :re] = options.delete(k)
215
+ o[k = :visibility] = options.delete(k)
216
+ raise ArgumentError, "Unknown option(s): #{options.inspect}" if not options.empty?
217
+
218
+ k = :access; raise ArgumentError, "options[#{k.inspect}] must be in #{GLM_ALL_ACCESSES.inspect}, #{o[k].inspect} given" if o[k] and not GLM_ALL_ACCESSES.include? o[k]
219
+ k = :fullre; raise ArgumentError, "options[#{k.inspect}] must be Regexp, #{o[k].class} given" if o[k] and not o[k].is_a? Regexp
220
+
221
+ k = :objs
222
+ raise ArgumentError, "options[#{k.inspect}] must be set" if not o[k]
223
+ raise ArgumentError, "options[#{k.inspect}] must be Array, #{o[k].class} given" if o[k] and not o[k].is_a? Array
224
+
225
+ k = :re; raise ArgumentError, "options[#{k.inspect}] must be Regexp, #{o[k].class} given" if o[k] and not o[k].is_a? Regexp
226
+
227
+ if o[k = :visibility]
228
+ o[k] = [o[k]].flatten
229
+ o[k].each do |v|
230
+ raise ArgumentError, "options[#{k.inspect}] must be in #{GLM_ALL_VISIBILITIES.inspect}, #{v.inspect} given" if not GLM_ALL_VISIBILITIES.include? v
231
+ end
232
+ end
233
+
234
+ # NOTE: `:all` and `:own` are NOT mutually exclusive. They are mutually confusive. :)
235
+
236
+ # Build per-obj lists.
237
+ per_obj = o[:objs].uniq.map do |obj|
238
+ ar = []
239
+
240
+ Tools.get_methods(obj).each do |inspector, methods|
241
+ ar += methods.map {|method_name| ListMethod.new(:obj => obj, :inspector => inspector, :method_name => method_name)}
242
+ end
243
+
244
+ # Filter by access if requested.
245
+ ar.reject! {|r| r.access != o[:access]} if o[:access]
246
+
247
+ # Filter by visibility if requested.
248
+ ar.reject! {|r| o[:visibility].none? {|vis| r.visibility == vis}} if o[:visibility]
249
+
250
+ # Leave only own methods if requested.
251
+ ar.reject! {|r| not r.own?} if o[:own]
252
+
253
+ # Apply RE if requested.
254
+ ar.reject! {|r| not r.match(o[:re])} if o[:re]
255
+
256
+ # Apply full RE if requested
257
+ ar.reject! {|r| not r.fullmatch(o[:fullre])} if o[:fullre]
258
+
259
+ # Apply smart filters if requested.
260
+ ar = apply_smart_filters(obj, ar) if not o[:all]
261
+
262
+ # Important, return `ar` from block.
263
+ ar
264
+ end # o[:objs].each
265
+
266
+ out = per_obj.flatten(1)
267
+ ##p "out.size", out.size
268
+
269
+ # Chop off duplicates.
270
+ out.uniq!
271
+
272
+ # DO NOT sort by default. If required for visual listing, that's caller's responsibility!
273
+ #out.sort!
274
+
275
+ out
276
+ end
277
+
278
+ # Used in <tt>do_history</tt>.
279
+ # Get prefix of the last "subject.ri args" command.
280
+ # Return everything before " args" or <tt>nil</tt> if command didn't have arguments.
281
+ def self.get_ri_arg_prefix(cmd)
282
+ if (mat = cmd.match /\A(\s*.+?\.ri)\s+\S/)
283
+ mat[1]
284
+ end
285
+ end
286
+
287
+ # Return local library instance.
288
+ def self.library
289
+ @lib ||= Library.new
290
+
291
+ # Update sensitive attrs on every call.
292
+ @lib.frontend = ::ORI.conf.frontend
293
+ @lib.shell_escape = ::ORI.conf.shell_escape
294
+
295
+ @lib
296
+ end
297
+
298
+ # Show content in a configured pager.
299
+ #
300
+ # pager do |f|
301
+ # f.puts "Hello, world!"
302
+ # end
303
+ def self.pager(&block)
304
+ IO.popen(::ORI.conf.pager, "w", &block)
305
+ end
306
+
307
+ # Do main job.
308
+ def self.ri(obj, *args)
309
+ # Most of the time return nil, for list modes return number of items. Could be useful. Don't return `false` on error, that's confusing.
310
+ out = nil
311
+
312
+ begin
313
+ # Build request.
314
+ req = ::ORI::Request.parse(*args)
315
+ raise UserError, "Bad request: #{req.message}" if req.error?
316
+ ##IrbHacks.break req
317
+
318
+ # List request.
319
+ #
320
+ # Klass.ri //
321
+ if req.list?
322
+ begin
323
+ req.glm_options[:objs].unshift(obj)
324
+ list_methods = get_list_methods(req.glm_options).sort
325
+ rescue ArgumentError => e
326
+ raise UserError, "Bad request: #{e.message}"
327
+ end
328
+ raise UserError, "No methods found" if list_methods.size < 1
329
+
330
+ # Display.
331
+ pager do |f|
332
+ f.puts list_methods.map {|r| r.format(:color => ::ORI.conf.color)}
333
+ end
334
+
335
+ out = list_methods.size
336
+ raise Break
337
+ end # if req.list?
338
+
339
+ # Class or method request. Particular ri article should be displayed.
340
+ #
341
+ # Klass.ri
342
+ # Klass.ri :meth
343
+ mam_topics = if req.self?
344
+ [[Tools.get_module_name(obj.is_a?(Module) ? obj : obj.class)]]
345
+ elsif req.method?
346
+ begin
347
+ req.glm_options[:objs].unshift(obj)
348
+ list_methods = get_list_methods(req.glm_options)
349
+ rescue ArgumentError => e
350
+ raise UserError, "Bad request: #{e.message}"
351
+ end
352
+ raise UserError, "No methods found" if list_methods.size < 1
353
+
354
+ # Collect topics.
355
+ # NOTE: `uniq` is important. Take `Module#public` as an example.
356
+ list_methods.map {|r| r.ri_topics}.flatten(1).uniq
357
+ else
358
+ raise "Unrecognized request kind #{req.kind.inspect}, SE"
359
+ end # mam_topics =
360
+
361
+ # Lookup topics. Display progress -- 1 character per lookup.
362
+ print colorize([:message, :action], "Looking up topics [", [:reset], mam_topics.map {|ar| colorize_mam(ar)}.join(", "), [:message, :action], "] ", [:reset])
363
+
364
+ found = []
365
+ mam_topics.each do |mam|
366
+ topic = mam.join
367
+ content = library.lookup(topic)
368
+ if content
369
+ print "o"
370
+ found << {
371
+ :topic => colorize_mam(mam),
372
+ :content => content,
373
+ }
374
+ else
375
+ print "."
376
+ end
377
+ end
378
+ puts
379
+
380
+ raise UserError, "No articles found" if found.size < 1
381
+
382
+ # Decide which article to show.
383
+ content = if found.size == 1
384
+ found.first[:content]
385
+ else
386
+ items = found.map {|h| ["#{h[:topic]} (#{h[:content].size}b)", h[:content]]}
387
+ choice(items, {
388
+ :colorize_labels => false,
389
+ :title => "More than 1 article found",
390
+ :on_skip => items.first[1],
391
+ })
392
+ end
393
+
394
+ # Handle abort.
395
+ raise Break if not content
396
+
397
+ # Display.
398
+ pager do |f|
399
+ f.puts content
400
+ end
401
+ rescue UserError => e
402
+ puts colorize([:message, :error], e.message, [:reset])
403
+
404
+ out = nil
405
+ rescue Break
406
+ end
407
+
408
+ out
409
+ end
410
+ end # Internals
411
+ end # ORI