class RDoc::RI::Driver

def self.default_options

def self.default_options
  options = {}
  options[:use_stdout] = !$stdout.tty?
  options[:width] = 72
  options[:interactive] = false
  options[:use_cache] = true
  options[:profile] = false
  # By default all standard paths are used.
  options[:use_system] = true
  options[:use_site] = true
  options[:use_home] = true
  options[:use_gems] = true
  options[:extra_doc_dirs] = []
  return options
end

def self.dump data_path

def self.dump data_path
  require 'pp'
  open data_path, 'rb' do |io|
    pp Marshal.load(io.read)
  end
end

def self.process_args argv

def self.process_args argv
  options = default_options
  opts = OptionParser.new do |opt|
    opt.accept File do |file,|
      File.readable?(file) and not File.directory?(file) and file
    end
    opt.program_name = File.basename $0
    opt.version = RDoc::VERSION
    opt.release = nil
    opt.summary_indent = ' ' * 4
    opt.banner = <<-EOT
age: #{opt.program_name} [options] [names...]
ere name can be:
Class | Class::method | Class#method | Class.method | method
l class names may be abbreviated to their minimum unambiguous form. If a name
 ambiguous, all valid options will be listed.
e form '.' method matches either class or instance methods, while #method
tches only instance and ::method matches only class methods.
r example:
  #{opt.program_name} Fil
  #{opt.program_name} File
  #{opt.program_name} File.new
  #{opt.program_name} zip
te that shell quoting may be required for method names containing
nctuation:
  #{opt.program_name} 'Array.[]'
  #{opt.program_name} compact\\!
 see the default directories ri will search, run:
  #{opt.program_name} --list-doc-dirs
ecifying the --system, --site, --home, --gems or --doc-dir options will
mit ri to searching only the specified directories.
tions may also be set in the 'RI' environment variable.
    EOT
    opt.separator nil
    opt.separator "Options:"
    opt.separator nil
    formatters = RDoc::Markup.constants.grep(/^To[A-Z][a-z]+$/).sort
    formatters = formatters.sort.map do |formatter|
      formatter.to_s.sub('To', '').downcase
    end
    opt.on("--format=NAME", "-f",
           "Uses the selected formatter. The default",
           "formatter is bs for paged output and ansi",
           "otherwise. Valid formatters are:",
           formatters.join(' '), formatters) do |value|
      options[:formatter] = RDoc::Markup.const_get "To#{value.capitalize}"
    end
    opt.separator nil
    opt.on("--no-pager", "-T",
           "Send output directly to stdout,",
           "rather than to a pager.") do
      options[:use_stdout] = true
    end
    opt.separator nil
    opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger,
           "Set the width of the output.") do |value|
      options[:width] = value
    end
    opt.separator nil
    opt.on("--interactive", "-i",
           "In interactive mode you can repeatedly",
           "look up methods with autocomplete.") do
      options[:interactive] = true
    end
    opt.separator nil
    opt.on("--[no-]profile",
           "Run with the ruby profiler") do |value|
      options[:profile] = value
    end
    opt.separator nil
    opt.separator "Data source options:"
    opt.separator nil
    opt.on("--list-doc-dirs",
           "List the directories from which ri will",
           "source documentation on stdout and exit.") do
      options[:list_doc_dirs] = true
    end
    opt.separator nil
    opt.on("--doc-dir=DIRNAME", "-d", Array,
           "List of directories from which to source",
           "documentation in addition to the standard",
           "directories.  May be repeated.") do |value|
      value.each do |dir|
        unless File.directory? dir then
          raise OptionParser::InvalidArgument, "#{dir} is not a directory"
        end
        options[:extra_doc_dirs] << File.expand_path(dir)
      end
    end
    opt.separator nil
    opt.on("--no-standard-docs",
           "Do not include documentation from",
           "the Ruby standard library, site_lib,",
           "installed gems, or ~/.rdoc.",
           "Use with --doc-dir") do
      options[:use_system] = false
      options[:use_site] = false
      options[:use_gems] = false
      options[:use_home] = false
    end
    opt.separator nil
    opt.on("--[no-]system",
           "Include documentation from Ruby's standard",
           "library.  Defaults to true.") do |value|
      options[:use_system] = value
    end
    opt.separator nil
    opt.on("--[no-]site",
           "Include documentation from libraries",
           "installed in site_lib.",
           "Defaults to true.") do |value|
      options[:use_site] = value
    end
    opt.separator nil
    opt.on("--[no-]gems",
           "Include documentation from RubyGems.",
           "Defaults to true.") do |value|
      options[:use_gems] = value
    end
    opt.separator nil
    opt.on("--[no-]home",
           "Include documentation stored in ~/.rdoc.",
           "Defaults to true.") do |value|
      options[:use_home] = value
    end
    opt.separator nil
    opt.separator "Debug options:"
    opt.separator nil
    opt.on("--dump=CACHE", File,
           "Dumps data from an ri cache or data file") do |value|
      options[:dump_path] = value
    end
  end
  argv = ENV['RI'].to_s.split.concat argv
  opts.parse! argv
  options[:names] = argv
  options[:use_stdout] ||= !$stdout.tty?
  options[:use_stdout] ||= options[:interactive]
  options[:width] ||= 72
  options
rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
  puts opts
  puts
  puts e
  exit 1
end

def self.run argv = ARGV

def self.run argv = ARGV
  options = process_args argv
  if options[:dump_path] then
    dump options[:dump_path]
    return
  end
  ri = new options
  ri.run
end

def add_also_in out, also_in

def add_also_in out, also_in
  return if also_in.empty?
  out << RDoc::Markup::Rule.new(1)
  out << RDoc::Markup::Paragraph.new("Also found in:")
  paths = RDoc::Markup::Verbatim.new
  also_in.each do |store|
    paths.parts.push '  ', store.friendly_path, "\n"
  end
  out << paths
end

def add_class out, name, classes

def add_class out, name, classes
  heading = if classes.all? { |klass| klass.module? } then
              name
            else
              superclass = classes.map do |klass|
                klass.superclass unless klass.module?
              end.compact.shift || 'Object'
              "#{name} < #{superclass}"
            end
  out << RDoc::Markup::Heading.new(1, heading)
  out << RDoc::Markup::BlankLine.new
end

def add_from out, store

def add_from out, store
  out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
end

def add_includes out, includes

def add_includes out, includes
  return if includes.empty?
  out << RDoc::Markup::Rule.new(1)
  out << RDoc::Markup::Heading.new(1, "Includes:")
  includes.each do |modules, store|
    if modules.length == 1 then
      include = modules.first
      name = include.name
      path = store.friendly_path
      out << RDoc::Markup::Paragraph.new("#{name} (from #{path})")
      if include.comment then
        out << RDoc::Markup::BlankLine.new
        out << include.comment
      end
    else
      out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
      wout, with = modules.partition { |incl| incl.comment.empty? }
      out << RDoc::Markup::BlankLine.new unless with.empty?
      with.each do |incl|
        out << RDoc::Markup::Paragraph.new(incl.name)
        out << RDoc::Markup::BlankLine.new
        out << incl.comment
      end
      unless wout.empty? then
        verb = RDoc::Markup::Verbatim.new
        wout.each do |incl|
          verb.push '  ', incl.name, "\n"
        end
        out << verb
      end
    end
  end
end

def add_method_list out, methods, name

def add_method_list out, methods, name
  return unless methods
  out << RDoc::Markup::Heading.new(1, "#{name}:")
  out << RDoc::Markup::BlankLine.new
  out.push(*methods.map do |method|
    RDoc::Markup::Verbatim.new '  ', method
  end)
  out << RDoc::Markup::BlankLine.new
end

def ancestors_of klass

def ancestors_of klass
  ancestors = []
  unexamined = [klass]
  seen = []
  loop do
    break if unexamined.empty?
    current = unexamined.shift
    seen << current
    stores = classes[current]
    break unless stores and not stores.empty?
    klasses = stores.map do |store|
      store.ancestors[current]
    end.flatten.uniq
    klasses = klasses - seen
    ancestors.push(*klasses)
    unexamined.push(*klasses)
  end
  ancestors.reverse
end

def class_cache # :nodoc:

:nodoc:
def class_cache # :nodoc:
end

def classes

def classes
  return @classes if @classes
  @classes = {}
  @stores.each do |store|
    store.cache[:modules].each do |mod|
      # using default block causes searched-for modules to be added
      @classes[mod] ||= []
      @classes[mod] << store
    end
  end
  @classes
end

def complete name

def complete name
  klasses = classes.keys
  completions = []
  klass, selector, method = parse_name name
  # may need to include Foo when given Foo::
  klass_name = method ? name : klass
  if name !~ /#|\./ then
    completions.push(*klasses.grep(/^#{klass_name}/))
  elsif selector then
    completions << klass if classes.key? klass
  elsif classes.key? klass_name then
    completions << klass_name
  end
  if completions.include? klass and name =~ /#|\.|::/ then
    methods = list_methods_matching name
    if not methods.empty? then
      # remove Foo if given Foo:: and a method was found
      completions.delete klass
    elsif selector then
      # replace Foo with Foo:: as given
      completions.delete klass
      completions << "#{klass}#{selector}"
    end
    completions.push(*methods)
  end
  completions.sort
end

def display document

def display document
  page do |io|
    text = document.accept formatter
    io.write text
  end
end

def display_class name

def display_class name
  return if name =~ /#|\./
  klasses = []
  includes = []
  found = @stores.map do |store|
    begin
      klass = store.load_class name
      klasses  << klass
      includes << [klass.includes, store] if klass.includes
      [store, klass]
    rescue Errno::ENOENT
    end
  end.compact
  return if found.empty?
  also_in = []
  includes.reject! do |modules,| modules.empty? end
  out = RDoc::Markup::Document.new
  add_class out, name, klasses
  add_includes out, includes
  found.each do |store, klass|
    comment = klass.comment
    class_methods    = store.class_methods[klass.full_name]
    instance_methods = store.instance_methods[klass.full_name]
    attributes       = store.attributes[klass.full_name]
    if comment.empty? and !(instance_methods or class_methods) then
      also_in << store
      next
    end
    add_from out, store
    unless comment.empty? then
      out << RDoc::Markup::Rule.new(1)
      out << comment
    end
    if class_methods or instance_methods or not klass.constants.empty? then
      out << RDoc::Markup::Rule.new
    end
    unless klass.constants.empty? then
      out << RDoc::Markup::Heading.new(1, "Constants:")
      out << RDoc::Markup::BlankLine.new
      list = RDoc::Markup::List.new :NOTE
      constants = klass.constants.sort_by { |constant| constant.name }
      list.push(*constants.map do |constant|
        parts = constant.comment.parts if constant.comment
        parts << RDoc::Markup::Paragraph.new('[not documented]') if
          parts.empty?
        RDoc::Markup::ListItem.new(constant.name, *parts)
      end)
      out << list
    end
    add_method_list out, class_methods,    'Class methods'
    add_method_list out, instance_methods, 'Instance methods'
    add_method_list out, attributes,       'Attributes'
    out << RDoc::Markup::BlankLine.new
  end
  add_also_in out, also_in
  display out
end

def display_method name

def display_method name
  found = load_methods_matching name
  raise NotFoundError, name if found.empty?
  out = RDoc::Markup::Document.new
  out << RDoc::Markup::Heading.new(1, name)
  out << RDoc::Markup::BlankLine.new
  found.each do |store, methods|
    methods.each do |method|
      out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
      unless name =~ /^#{Regexp.escape method.parent_name}/ then
        out << RDoc::Markup::Heading.new(3, "Implementation from #{method.parent_name}")
      end
      out << RDoc::Markup::Rule.new(1)
      if method.arglists then
        arglists = method.arglists.chomp.split "\n"
        arglists = arglists.map { |line| ['  ', line, "\n"] }
        out << RDoc::Markup::Verbatim.new(*arglists.flatten)
        out << RDoc::Markup::Rule.new(1)
      end
      out << RDoc::Markup::BlankLine.new
      out << method.comment
      out << RDoc::Markup::BlankLine.new
    end
  end
  display out
end

def display_name name

def display_name name
  return true if display_class name
  display_method name if name =~ /::|#|\./
  true
rescue NotFoundError
  matches = list_methods_matching name if name =~ /::|#|\./
  matches = classes.keys.grep(/^#{name}/) if matches.empty?
  raise if matches.empty?
  page do |io|
    io.puts "#{name} not found, maybe you meant:"
    io.puts
    io.puts matches.join("\n")
  end
  false
end

def display_names names

def display_names names
  names.each do |name|
    name = expand_name name
    display_name name
  end
end

def expand_class klass

def expand_class klass
  klass.split('::').inject '' do |expanded, klass_part|
    expanded << '::' unless expanded.empty?
    short = expanded << klass_part
    subset = classes.keys.select do |klass_name|
      klass_name =~ /^#{expanded}[^:]*$/
    end
    abbrevs = Abbrev.abbrev subset
    expanded = abbrevs[short]
    raise NotFoundError, short unless expanded
    expanded.dup
  end
end

def expand_name name

def expand_name name
  klass, selector, method = parse_name name
  return [selector, method].join if klass.empty?
  "#{expand_class klass}#{selector}#{method}"
end

def find_methods name

def find_methods name
  klass, selector, method = parse_name name
  types = method_type selector
  klasses = nil
  ambiguous = klass.empty?
  if ambiguous then
    klasses = classes.keys
  else
    klasses = ancestors_of klass
    klasses.unshift klass
  end
  methods = []
  klasses.each do |ancestor|
    ancestors = classes[ancestor]
    next unless ancestors
    klass = ancestor if ambiguous
    ancestors.each do |store|
      methods << [store, klass, ancestor, types, method]
    end
  end
  methods = methods.sort_by do |_, k, a, _, m|
    [k, a, m].compact
  end
  methods.each do |item|
    yield(*item) # :yields: store, klass, ancestor, types, method
  end
  self
end

def formatter

def formatter
  if @formatter_klass then
    @formatter_klass.new
  elsif paging? then
    RDoc::Markup::ToBs.new
  else
    RDoc::Markup::ToAnsi.new
  end
end

def in_path? file

def in_path? file
  return true if file =~ %r%\A/% and File.exist? file
  ENV['PATH'].split(File::PATH_SEPARATOR).any? do |path|
    File.exist? File.join(path, file)
  end
end

def initialize initial_options = {}

def initialize initial_options = {}
  @paging = false
  @classes = nil
  options = self.class.default_options.update(initial_options)
  @formatter_klass = options[:formatter]
  require 'profile' if options[:profile]
  @names = options[:names]
  @doc_dirs = []
  @stores   = []
  RDoc::RI::Paths.each(options[:use_system], options[:use_site],
                                 options[:use_home], options[:use_gems],
                                 *options[:extra_doc_dirs]) do |path, type|
    @doc_dirs << path
    store = RDoc::RI::Store.new path, type
    store.load_cache
    @stores << store
  end
  @list_doc_dirs = options[:list_doc_dirs]
  @interactive = options[:interactive]
  @use_stdout  = options[:use_stdout]
end

def interactive

def interactive
  puts "\nEnter the method name you want to look up."
  if defined? Readline then
    Readline.completion_proc = method :complete
    puts "You can use tab to autocomplete."
  end
  puts "Enter a blank line to exit.\n\n"
  loop do
    name = if defined? Readline then
             Readline.readline ">> "
           else
             print ">> "
             $stdin.gets
           end
    return if name.nil? or name.empty?
    name = expand_name name.strip
    begin
      display_name name
    rescue NotFoundError => e
      puts e.message
    end
  end
rescue Interrupt
  exit
end

def list_known_classes

def list_known_classes
  classes = []
  stores.each do |store|
    classes << store.modules
  end
  classes = classes.flatten.uniq.sort
  page do |io|
    if paging? or io.tty? then
      io.puts "Classes and Modules known to ri:"
      io.puts
    end
    io.puts classes.join("\n")
  end
end

def list_methods_matching name

def list_methods_matching name
  found = []
  find_methods name do |store, klass, ancestor, types, method|
    if types == :instance or types == :both then
      methods = store.instance_methods[ancestor]
      if methods then
        matches = methods.grep(/^#{method}/)
        matches = matches.map do |match|
          "#{klass}##{match}"
        end
        found.push(*matches)
      end
    end
    if types == :class or types == :both then
      methods = store.class_methods[ancestor]
      next unless methods
      matches = methods.grep(/^#{method}/)
      matches = matches.map do |match|
        "#{klass}::#{match}"
      end
      found.push(*matches)
    end
  end
  found.uniq
end

def load_method store, cache, klass, type, name

def load_method store, cache, klass, type, name
  methods = store.send(cache)[klass]
  return unless methods
  method = methods.find do |method_name|
    method_name == name
  end
  return unless method
  store.load_method klass, "#{type}#{method}"
end

def load_methods_matching name

def load_methods_matching name
  found = []
  find_methods name do |store, klass, ancestor, types, method|
    methods = []
    methods << load_method(store, :class_methods, ancestor, '::',  method) if
      types == :class or types == :both
    methods << load_method(store, :instance_methods, ancestor, '#',  method) if
      types == :instance or types == :both
    found << [store, methods.compact]
  end
  found.reject do |path, methods| methods.empty? end
end

def method_type selector

def method_type selector
  case selector
  when '.', nil then :both
  when '#'      then :instance
  else               :class
  end
end

def page

def page
  if pager = setup_pager then
    begin
      yield pager
    ensure
      pager.close
    end
  else
    yield $stdout
  end
rescue Errno::EPIPE
ensure
  @paging = false
end

def paging?

def paging?
  @paging
end

def parse_name(name)

def parse_name(name)
  parts = name.split(/(::|#|\.)/)
  if parts.length == 1 then
    if parts.first =~ /^[a-z]/ then
      type = '.'
      meth = parts.pop
    else
      type = nil
      meth = nil
    end
  elsif parts.length == 2 or parts.last =~ /::|#|\./ then
    type = parts.pop
    meth = nil
  elsif parts[-2] != '::' or parts.last !~ /^[A-Z]/ then
    meth = parts.pop
    type = parts.pop
  end
  klass = parts.join
  [klass, type, meth]
end

def run

def run
  if @list_doc_dirs then
    puts @doc_dirs
  elsif @interactive then
    interactive
  elsif @names.empty? then
    list_known_classes
  else
    display_names @names
  end
rescue NotFoundError => e
  abort e.message
end

def setup_pager

def setup_pager
  return if @use_stdout
  pagers = [ENV['RI_PAGER'], ENV['PAGER'], 'pager', 'less', 'more']
  pagers.compact.uniq.each do |pager|
    pager_cmd = pager.split.first
    next unless in_path? pager_cmd
    io = IO.popen(pager, 'w') rescue next
    next if $? and $?.exited? # pager didn't work
    @paging = true
    return io
  end
  @use_stdout = true
  nil
end