ori 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,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