PageRenderTime 50ms CodeModel.GetById 21ms app.highlight 26ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/wirble/lib/wirble.rb

http://github.com/alloy/dietrb
Ruby | 541 lines | 367 code | 57 blank | 117 comment | 33 complexity | d5e5715db619a8e62fd35caa539c57de MD5 | raw file
  1require 'ostruct'
  2
  3#
  4# Wirble: A collection of useful Irb features.
  5#
  6# To use, add the following to your ~/.irbrc:
  7#
  8#   require 'rubygems'
  9#   require 'wirble'
 10#   Wirble.init
 11#
 12# If you want color in Irb, add this to your ~/.irbrc as well:
 13#
 14#   Wirble.colorize
 15#
 16# Note:  I spent a fair amount of time documenting this code in the
 17# README.  If you've installed via RubyGems, root around your cache a
 18# little bit (or fire up gem_server) and read it before you tear your
 19# hair out sifting through the code below.
 20# 
 21module Wirble
 22  VERSION = '0.1.3.2'
 23
 24  #
 25  # Load internal Ruby features, including pp, tab-completion, 
 26  # and a simple prompt.
 27  #
 28  module Internals
 29    # list of internal libraries to automatically load
 30    LIBRARIES = %w{pp irb/completion}
 31
 32    #
 33    # load libraries
 34    #
 35    def self.init_libraries
 36      LIBRARIES.each do |lib| 
 37        begin
 38          require lib 
 39        rescue LoadError
 40          nil
 41        end
 42      end
 43    end
 44
 45    #
 46    # Set a simple prompt, unless a custom one has been specified.
 47    #
 48    def self.init_prompt
 49      # set the prompt
 50      if IRB.conf[:PROMPT_MODE] == :DEFAULT
 51        IRB.conf[:PROMPT_MODE] = :SIMPLE
 52      end
 53    end
 54
 55    #
 56    # Load all Ruby internal features.
 57    #
 58    def self.init(opt = nil)
 59      init_libraries unless opt && opt[:skip_libraries]
 60      init_prompt unless opt && opt[:skip_prompt]
 61    end
 62  end
 63
 64  #
 65  # Basic IRB history support.  This is based on the tips from 
 66  # http://wiki.rubygarden.org/Ruby/page/show/Irb/TipsAndTricks
 67  #
 68  class History
 69    DEFAULTS = {
 70      :history_path   => ENV['IRB_HISTORY_FILE'] || "~/.irb_history",
 71      :history_size   => (ENV['IRB_HISTORY_SIZE'] || 1000).to_i,
 72      :history_perms  => File::WRONLY | File::CREAT | File::TRUNC,
 73      :history_uniq   => true,
 74    }
 75 
 76    private
 77
 78    def say(*args)
 79      puts(*args) if @verbose
 80    end
 81
 82    def cfg(key)
 83      @opt["history_#{key}".intern]
 84    end
 85
 86    def save_history
 87      path, max_size, perms, uniq = %w{path size perms uniq}.map { |v| cfg(v) }
 88
 89      # read lines from history, and truncate the list (if necessary)
 90      lines = Readline::HISTORY.to_a
 91
 92      lines.reverse! if reverse = uniq.to_s == 'reverse'
 93      lines.uniq!    if uniq
 94      lines.reverse! if reverse
 95
 96      lines.slice!(0, lines.size - max_size) if lines.size > max_size
 97
 98      # write the history file
 99      real_path = File.expand_path(path)
100      File.open(real_path, perms) { |fh| fh.puts lines }
101      say 'Saved %d lines to history file %s.' % [lines.size, path]
102    end
103
104    def load_history
105      # expand history file and make sure it exists
106      real_path = File.expand_path(cfg('path'))
107      unless File.exist?(real_path)
108        say "History file #{real_path} doesn't exist."
109        return
110      end
111
112      # read lines from file and add them to history
113      lines = File.readlines(real_path).map { |line| line.chomp }
114      Readline::HISTORY.push(*lines)
115
116      say 'Read %d lines from history file %s' % [lines.size, cfg('path')]
117    end
118
119    public
120
121    def initialize(opt = nil)
122      @opt = DEFAULTS.merge(opt || {})
123      return unless defined? Readline::HISTORY
124      load_history
125      Kernel.at_exit { save_history }
126    end
127  end
128
129  #
130  # Add color support to IRB.
131  #
132  module Colorize
133    #
134    # Tokenize an inspection string.
135    #
136    module Tokenizer
137      def self.tokenize(str)
138        raise 'missing block' unless block_given?
139        chars = str.split(//)
140
141        # $stderr.puts "DEBUG: chars = #{chars.join(',')}"
142
143        state, val, i, lc = [], '', 0, nil
144        while i <= chars.size
145          repeat = false
146          c = chars[i]
147
148          # $stderr.puts "DEBUG: state = #{state}"
149
150          case state[-1]
151          when nil
152            case c
153            when ':'
154              state << :symbol
155            when '"'
156              state << :string
157            when '#'
158              state << :object
159            when /[a-z]/i
160              state << :keyword
161              repeat = true
162            when /[0-9-]/
163              state << :number
164              repeat = true
165            when '{'
166              yield :open_hash, '{'
167            when '['
168              yield :open_array, '['
169            when ']'
170              yield :close_array, ']'
171            when '}'
172              yield :close_hash, '}'
173            when /\s/
174              yield :whitespace, c
175            when ','
176              yield :comma, ','
177            when '>'
178              yield :refers, '=>' if lc == '='
179            when '.'
180              yield :range, '..' if lc == '.'
181            when '='
182              # ignore these, they're used elsewhere
183              nil
184            else 
185              # $stderr.puts "DEBUG: ignoring char #{c}"
186            end
187          when :symbol
188            case c
189            # XXX: should have =, but that messes up foo=>bar
190            when /[a-z0-9_!?]/
191              val << c
192            else
193              yield :symbol_prefix, ':'
194              yield state[-1], val
195              state.pop; val = ''
196              repeat = true
197            end
198          when :string
199            case c
200            when '"'
201              if lc == "\\"
202                val[-1] = ?"
203              else
204                yield :open_string, '"'
205                yield state[-1], val
206                state.pop; val = ''
207                yield :close_string, '"'
208              end
209            else
210              val << c
211            end
212          when :keyword
213            case c
214            when /[a-z0-9_]/i
215              val << c
216            else
217              # is this a class?
218              st = val =~ /^[A-Z]/ ? :class : state[-1]
219
220              yield st, val
221              state.pop; val = ''
222              repeat = true
223            end
224          when :number
225            case c
226            when /[0-9e-]/
227              val << c
228            when '.'
229              if lc == '.'
230                val[/\.$/] = ''
231                yield state[-1], val
232                state.pop; val = ''
233                yield :range, '..'
234              else
235                val << c
236              end
237            else
238              yield state[-1], val
239              state.pop; val = ''
240              repeat = true
241            end
242          when :object
243            case c
244            when '<' 
245              yield :open_object, '#<'
246              state << :object_class
247            when ':' 
248              state << :object_addr
249            when '@' 
250              state << :object_line
251            when '>'
252              yield :close_object, '>'
253              state.pop; val = ''
254            end
255          when :object_class
256            case c
257            when ':'
258              yield state[-1], val
259              state.pop; val = ''
260              repeat = true
261            else
262              val << c
263            end
264          when :object_addr
265            case c
266            when '>'
267            when '@'
268              yield :object_addr_prefix, ':'
269              yield state[-1], val
270              state.pop; val = ''
271              repeat = true
272            else
273              val << c
274            end
275          when :object_line
276            case c
277            when '>'
278              yield :object_line_prefix, '@'
279              yield state[-1], val
280              state.pop; val = ''
281              repeat = true
282            else
283              val << c
284            end
285          else
286            raise "unknown state #{state}"
287          end
288
289          unless repeat
290            i += 1
291            lc = c
292          end
293        end
294      end
295    end
296
297    #
298    # Terminal escape codes for colors.
299    #
300    module Color
301      COLORS = {
302        :nothing      => '0;0',
303        :black        => '0;30',
304        :red          => '0;31',
305        :green        => '0;32',
306        :brown        => '0;33',
307        :blue         => '0;34',
308        :cyan         => '0;36',
309        :purple       => '0;35',
310        :light_gray   => '0;37',
311        :dark_gray    => '1;30',
312        :light_red    => '1;31',
313        :light_green  => '1;32',
314        :yellow       => '1;33',
315        :light_blue   => '1;34',
316        :light_cyan   => '1;36',
317        :light_purple => '1;35',
318        :white        => '1;37',
319      }
320      
321      #
322      # Return the escape code for a given color.
323      #
324      def self.escape(key)
325        COLORS.key?(key) && "\033[#{COLORS[key]}m"
326      end
327    end
328
329    #
330    # Default Wirble color scheme.
331    # 
332    DEFAULT_COLORS = {
333      # delimiter colors
334      :comma              => :blue,
335      :refers             => :blue,
336
337      # container colors (hash and array)
338      :open_hash          => :green,
339      :close_hash         => :green,
340      :open_array         => :green,
341      :close_array        => :green,
342
343      # object colors
344      :open_object        => :light_red,
345      :object_class       => :white,
346      :object_addr_prefix => :blue,
347      :object_line_prefix => :blue,
348      :close_object       => :light_red,
349
350      # symbol colors
351      :symbol             => :yellow,
352      :symbol_prefix      => :yellow,
353
354      # string colors
355      :open_string        => :red,
356      :string             => :cyan,
357      :close_string       => :red,
358
359      # misc colors
360      :number             => :cyan,
361      :keyword            => :green,
362      :class              => :light_green,
363      :range              => :red,
364    }
365
366    #
367    # Fruity testing colors.
368    # 
369    TESTING_COLORS = {
370      :comma            => :red,
371      :refers           => :red,
372      :open_hash        => :blue,
373      :close_hash       => :blue,
374      :open_array       => :green,
375      :close_array      => :green,
376      :open_object      => :light_red,
377      :object_class     => :light_green,
378      :object_addr      => :purple,
379      :object_line      => :light_purple,
380      :close_object     => :light_red,
381      :symbol           => :yellow,
382      :symbol_prefix    => :yellow,
383      :number           => :cyan,
384      :string           => :cyan,
385      :keyword          => :white,
386      :range            => :light_blue,
387    }
388
389    #
390    # Set color map to hash
391    # 
392    def self.colors=(hash)
393      @colors = hash
394    end
395
396    #
397    # Get current color map
398    # 
399    def self.colors
400      @colors ||= {}.update(DEFAULT_COLORS)
401    end
402
403    #
404    # Return a string with the given color.
405    #
406    def self.colorize_string(str, color)
407      col, nocol = [color, :nothing].map { |key| Color.escape(key) }
408      col ? "#{col}#{str}#{nocol}" : str
409    end
410
411    #
412    # Colorize the results of inspect
413    # 
414    def self.colorize(str)
415      begin
416        ret, nocol = '', Color.escape(:nothing)
417        Tokenizer.tokenize(str) do |tok, val|
418          # c = Color.escape(colors[tok])
419          ret << colorize_string(val, colors[tok])
420        end
421        ret
422      rescue
423        # catch any errors from the tokenizer (just in case)
424        str
425      end
426    end
427
428    #
429    # Enable colorized IRB results.
430    # 
431    def self.enable(custom_colors = nil)
432      # if there's a better way to do this, I'm all ears.
433      ::IRB::Irb.class_eval do
434        alias :non_color_output_value  :output_value
435
436        def output_value
437          if @context.inspect?
438            val = Colorize.colorize(@context.last_value.inspect)
439            p val
440            printf @context.return_format, val
441          else
442            printf @context.return_format, @context.last_value
443          end
444        end
445      end
446
447      self.colors = custom_colors if custom_colors
448    end
449
450    #
451    # Disable colorized IRB results.
452    # 
453    def self.disable
454      ::IRB::Irb.class_eval do
455        alias :output_value  :non_color_output_value
456      end
457    end
458  end
459
460  #
461  # Convenient shortcut methods.
462  #
463  module Shortcuts
464    #
465    # Print object methods, sorted by name. (excluding methods that
466    # exist in the class Object) .
467    #
468    def po(o)
469      o.methods.sort - Object.methods
470    end
471
472    #
473    # Print object constants, sorted by name.
474    #
475    def poc(o)
476      o.constants.sort
477    end
478  end
479
480  #
481  # Convenient shortcut for ri
482  #
483  module RiShortcut
484    def self.init
485      Kernel.class_eval {
486        def ri(arg)
487           puts `ri '#{arg}'`
488        end
489      }
490
491      Module.instance_eval {
492         def ri(meth=nil)
493           if meth
494             if instance_methods(false).include? meth.to_s
495               puts `ri #{self}##{meth}`
496             else
497               super
498             end
499           else
500             puts `ri #{self}`
501           end
502         end
503      }
504    end
505  end
506
507
508
509  #
510  # Enable color results.
511  #
512  def self.colorize(custom_colors = nil)
513    Colorize.enable(custom_colors)
514  end
515
516  #
517  # Load everything except color.
518  #
519  def self.init(opt = nil)
520    # make sure opt isn't nil
521    opt ||= {}
522
523    # load internal irb/ruby features
524    Internals.init(opt) unless opt && opt[:skip_internals]
525
526    # load the history
527    History.new(opt) unless opt && opt[:skip_history]
528
529    # load shortcuts
530    unless opt && opt[:skip_shortcuts]
531      # load ri shortcuts
532      RiShortcut.init
533
534      # include common shortcuts
535      Object.class_eval { include Shortcuts }
536    end
537
538    colorize(opt[:colors]) if opt && opt[:init_colors]
539  end
540end
541