eigenclass logo
MAIN  Index  Search  Changes  PageRank  Login

IRB+RI: still room for improvement - better completion, method definition site discovery

Yes, everybody and his dog has hacked his .irbrc in order to use RI and complete methods. You've seen it in rubygarden and then in wirble.

But really, typing

 Object.ri "object_id"

is way too much work.

sheepman showed the way to combine method completion and RI, and here's my improvement on it: thanks to FastRI*1 being much faster than RI, you can discover where a method came from and get the corresponding documentation: irb-ri-completion.png

You use it like this:

>> 1.0.ri_[TAB]
1.0.ri_"%"        1.0.ri_class          1.0.ri_instance_variable_set   1.0.ri_respond_to?
1.0.ri_"*"         1.0.ri_clone          1.0.ri_instance_variables      1.0.ri_ri_
1.0.ri_"**"        1.0.ri_coerce         1.0.ri_integer?                1.0.ri_round
>> 1.0.ri_p[TAB]
1.0.ri_prec               1.0.ri_prec_i             1.0.ri_protected_methods  
1.0.ri_prec_f             1.0.ri_private_methods    1.0.ri_public_methods

For each #method, you'll get the corresponding #ri_method completion. You can also do

 >> r 'String#strip'

but now also with completion.

Finding out where the methods come from.

If you have

a = {}

with previous RI+irb hacks you might have tried

Hash.ri "inject"

Leaving the unneeded typing aside, you'd have obtained

Nothing known about Hash#inject

In other words: you'd have to know where the method comes from, that is, in which class/module it was defined. Now, that's easy for Enumerable#inject, but did you know about e.g. Precision#prec?

My irbrc hack looks for the method in all the ancestors of the receiver's class, so you don't need to know where the method you knew (maybe) nothing about (besides the name) to begin with comes from.

This is where FastRI enters the picture: the older RI can take over one second per lookup even when all your RI docs are cached and you don't hit the disk, and much longer (about 5 seconds for me) for the first one.

So

>> 1.0.ri_object_id

could take over 10 seconds with RI. It's under half a second for FastRI*2 -- and half of that time is actually spent formatting the description.

Indeed, when I use ri, I get this on my system:

>> t = Time.new; 1.0.ri_object_id; Time.new - t            #(cold cache)
------------------------------------------------------- Object#object_id
[...]
=> 10.798814
>> t = Time.new; 1.0.ri_object_id; Time.new - t          #(.yaml cached)
------------------------------------------------------- Object#object_id
[...]
=> 6.647412

RI is this slow because it searches all installed RubyGems packages.

The details

The finishing touches that make this more convenient:

  • normal completion lists aren't polluted, and you won't see lots of ri_* in them.
  • Module/Class objects are handled correctly
  • "operator" methods are escaped
 >> x = 1.0
 => 1.0
 >> x.ri_
 x.ri_"%"        x.ri_class          x.ri_instance_variable_set   x.ri_respond_to?
 x.ri_"*"        x.ri_clone          x.ri_instance_variables      x.ri_ri_
 x.ri_"**"       x.ri_coerce         x.ri_integer?                x.ri_round
 [...]
 >> x.ri_"%"
 ---------------------------------------------------------------- Float#%
      flt % other         => float
      flt.modulo(other)   => float
 [...]
 >> x = x.ceil
 => 1
 >> x.ri_"%"
 --------------------------------------------------------------- Fixnum#%
      fix % other         => Numeric
      fix.modulo(other)   => Numeric
 ------------------------------------------------------------------------
      Returns fix modulo other. See Numeric.divmod for more information.
  • you also get completion for the "traditional" documentation mode
 >> r 'String#do[TAB]
 'String#downcase!'  'String#downcase'   
 >> r 'String#downcase'
 -------------------------------------------------------- String#downcase
      str.downcase   => new_str
 ------------------------------------------------------------------------
      Returns a copy of str with all uppercase letters replaced with 
      their lowercase counterparts. The operation is locale 
      insensitive---only characters ``A'' to ``Z'' are affected.
 
         "hEllO".downcase   #=> "hello"

The code

require 'irb/completion'

module Kernel
  def r(arg)
    puts `fri "#{arg}"`
  end
  private :r
end

class Object
  def puts_ri_documentation_for(obj, meth)
    case self
    when Module
      candidates = ancestors.map{|klass| "#{klass}::#{meth}"}
      candidates.concat(class << self; ancestors end.map{|k| "#{k}##{meth}"})
    else
      candidates = self.class.ancestors.map{|klass|  "#{klass}##{meth}"}
    end
    candidates.each do |candidate|
      #puts "TRYING #{candidate}"
      desc = `fri '#{candidate}'`
      unless desc.chomp == "nil"
      # uncomment to use ri (and some patience)
      #desc = `ri -T '#{candidate}' 2>/dev/null`
      #unless desc.empty?
        puts desc
        return true
      end
    end
    false
  end
  private :puts_ri_documentation_for

  def method_missing(meth, *args, &block)
    if md = /ri_(.*)/.match(meth.to_s)
      unless puts_ri_documentation_for(self,md[1])
        "Ri doesn't know about ##{meth}"
      end
    else
      super
    end
  end

  def ri_(meth)
    unless puts_ri_documentation_for(self,meth.to_s)
      "Ri doesn't know about ##{meth}"
    end
  end
end

RICompletionProc = proc{|input|
  bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
  case input
  when /(\s*(.*)\.ri_)(.*)/
    pre = $1
    receiver = $2
    meth = $3 ? /\A#{Regexp.quote($3)}/ : /./ #}
    begin
      candidates = eval("#{receiver}.methods", bind).map do |m|
        case m
        when /[A-Za-z_]/; m
        else # needs escaping
          %{"#{m}"}
        end
      end
      candidates = candidates.grep(meth)
      candidates.map{|s| pre + s }
    rescue Exception
      candidates = []
    end
  when /([A-Z]\w+)#(\w*)/ #}
    klass = $1
    meth = $2 ? /\A#{Regexp.quote($2)}/ : /./
    candidates = eval("#{klass}.instance_methods(false)", bind)
    candidates = candidates.grep(meth)
    candidates.map{|s| "'" + klass + '#' + s + "'"}
  else
    IRB::InputCompletor::CompletionProc.call(input)
  end
}
#Readline.basic_word_break_characters= " \t\n\"\\'`><=;|&{("
Readline.basic_word_break_characters= " \t\n\\><=;|&"
Readline.completion_proc = RICompletionProc



testing new BBS - mfp (2006-11-06 (Mon) 05:19:12)

Now the form sinks to the bottom as new posts are added. This should make cross-threading less attractive (hah don't you love those "cross-thread violation" posts on ruby-talk?).


***** - rubikitch (2006-11-06 (****) 07:25:58)

iHelp does almost the same thing.

mfp 2006-11-06 (Mon) 12:01:57

I'd missed kig's iHelp (check it out).

After a quick look at the code, it seems to me it does several things the above hack doesn't:

  • it's self-contained
  • it can open the pertinent ruby-doc.org page in your browser
  • it can generate HTML
  • it can use ruby2ruby to display method definitions

On the other hand,

  • it doesn't do method completion
  • it relies on the same code as RI and is therefore fairly slow
  • (it's big)

When I release FastRI (and if the API stabilizes), I might send kig a patch to use FastRI::RiService instead.

kig 2006-11-27 (Mon) 15:45:37

Ooh, I like the method completion (just found this post today.) The RI slowness in IHelp only exhibits itself on require, after that lookups are sub-millisecond class.


******* - rubikitch (2006-11-06 (***) 18:30:36)

Edited by mfp: removed the repeated comment and reformatted code

class Object
  define_method('a_"%"'){1}
  def method_missing(meth, *args, &block)
    [meth, args]
  end
end
a_"*"                      # => [:a_, ["*"]]
a_"%"                      # => [:a_, ["%"]]
__send__ 'a_"*"'                # => [:"a_\"*\"", []]
__send__ 'a_"%"'                # => 1
print"a"
# >> a

What a nice hack it is!! I initially misunderstood a_"*" is a method, but actually a_ is a method and "*" is an argument. It is funny that this completion routine completes a method and an argument!


Hmmm - Steve (2006-11-16 (Thr) 14:57:50)

Can you post your Xdefaults/wmii theme? Thanks!

mfp 2006-11-16 (Thr) 15:11:13

Sure, here's the relevant part of my .Xdefaults:

! Rxvt
Rxvt*color0:   #000000
Rxvt*color1:   #A80000
Rxvt*color2:   #00A800
Rxvt*color3:   #A8A800
Rxvt*color4:   #0000A8
Rxvt*color5:   #A800A8
Rxvt*color6:   #00A8A8
Rxvt*color7:   #A8A8A8
 
Rxvt*color8:   #000054
Rxvt*color9:   #FF0054
Rxvt*color10:  #00FF54
Rxvt*color11:  #FFFF54
Rxvt*color12:  #0000FF
Rxvt*color13:  #FF00FF
Rxvt*color14:  #00FFFF
Rxvt*color15:  #FFFFFF
 
Rxvt*font: -windows-proggyclean-medium-r-normal--13-80-96-96-c-70-iso8859-1,fixed
Rxvt*boldFont:
Rxvt*background: Black
Rxvt*foreground: White
Rxvt*scrollBar: false
Rxvt*scrollTtyOutput: false
Rxvt*scrollTtyKeypress: true
Rxvt*scrollWithBuffer: true
Rxvt*saveLines: 2000

and this is the wmii theme I'm using (with ruby-wmii):

WMII::Configuration.define do
  border      1
  font         "-windows-proggytinysz-medium-r-normal--10-80-96-96-c-60-iso8859-1"
  #font        '-*-fixed-*-r-*-*-10-*-*-*-*-*-*-*'
# _why's
 #selcolors   '#FFFFFF #248047 #147027'
 #normcolors  '#4D4E4F #DDDDAA #FFFFCC'
# anthrazit
  selcolors     '#eeeeee #506070 #708090'
  normcolors    '#bbbbbb #222222 #000000'
...

Nice - Gavin (2007-01-28 (Sun) 15:38:46)

Nice work mfp! I look forward to trying this soon.

It occurred to me recently that another way to integrate IRB and FastRI would be to modify IRB to recognise calls to ri, something like

 if line.strip =~ /^ri\s+(.*)/
   call_fast_ri($1)
 else
   # normal IRB behaviour
 end

The benefit of that is that you can use RI without having to type those accursed quotes, as in

 irb> ri String.upcase       # instead of
                             # irb> ri 'String.upcase'

It seems like your work here is even more convenient than my idea, but I wonder what you think of it anyway.

Cheers, Gavin

mfp 2007-02-03 (Sat) 03:48:05

There's no reason we can't have both :)

I'm not sure IRB has got any hook to modify the input before it's processed though, would have to read the sources.



Last modified:2006/11/06 05:11:03
Keyword(s):[blog] [ruby] [frontpage] [irb] [ri] [completion] [irbrc] [snippet] [fastri]
References:

*1 soon to be released

*2 also keep in mind that I'm just launching several fri processes one after another, the actual lookups take virtually no time (something in the 1/100ths of a second), and what we're measuring is how long ruby takes to start and parse the code