summaryrefslogtreecommitdiff
path: root/lib/rdoc/ri/driver.rb
diff options
context:
space:
mode:
authordrbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2010-04-01 07:45:16 +0000
committerdrbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2010-04-01 07:45:16 +0000
commit46580b51477355fece514573c88cb67030f4a502 (patch)
tree779c1a64466643461b3daa4cd9a3548b84f0fd55 /lib/rdoc/ri/driver.rb
parent9b40cdfe8c973a061c5683ad78c283b9ddb8b2e9 (diff)
Import RDoc 2.5
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@27147 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/rdoc/ri/driver.rb')
-rw-r--r--lib/rdoc/ri/driver.rb1155
1 files changed, 772 insertions, 383 deletions
diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb
index 89534a5972..1829de3432 100644
--- a/lib/rdoc/ri/driver.rb
+++ b/lib/rdoc/ri/driver.rb
@@ -1,89 +1,77 @@
+require 'abbrev'
require 'optparse'
-require 'yaml'
+
+begin
+ require 'readline'
+rescue LoadError
+end
require 'rdoc/ri'
require 'rdoc/ri/paths'
-require 'rdoc/ri/formatter'
-require 'rdoc/ri/display'
-require 'fileutils'
require 'rdoc/markup'
-require 'rdoc/markup/to_flow'
+require 'rdoc/markup/formatter'
+require 'rdoc/text'
-class RDoc::RI::Driver
+##
+# For RubyGems backwards compatibility
- #
- # This class offers both Hash and OpenStruct functionality.
- # We convert from the Core Hash to this before calling any of
- # the display methods, in order to give the display methods
- # a cleaner API for accessing the data.
- #
- class OpenStructHash < Hash
- #
- # This method converts from a Hash to an OpenStructHash.
- #
- def self.convert(object)
- case object
- when Hash then
- new_hash = new # Convert Hash -> OpenStructHash
-
- object.each do |key, value|
- new_hash[key] = convert(value)
- end
+require 'rdoc/ri/formatter'
- new_hash
- when Array then
- object.map do |element|
- convert(element)
- end
- else
- object
- end
- end
-
- def merge_enums(other)
- other.each do |k, v|
- if self[k] then
- case v
- when Array then
- # HACK dunno
- if String === self[k] and self[k].empty? then
- self[k] = v
- else
- self[k] += v
- end
- when Hash then
- self[k].update v
- else
- # do nothing
- end
- else
- self[k] = v
- end
- end
- end
+##
+# The RI driver implements the command-line ri tool.
+#
+# The driver supports:
+# * loading RI data from:
+# * Ruby's standard library
+# * RubyGems
+# * ~/.rdoc
+# * A user-supplied directory
+# * Paging output (uses RI_PAGER environment variable, PAGER environment
+# variable or the less, more and pager programs)
+# * Interactive mode with tab-completion
+# * Abbreviated names (ri Zl shows Zlib documentation)
+# * Colorized output
+# * Merging output from multiple RI data sources
- def method_missing method, *args
- self[method.to_s]
- end
- end
+class RDoc::RI::Driver
+
+ ##
+ # Base Driver error class
class Error < RDoc::RI::Error; end
+ ##
+ # Raised when a name isn't found in the ri data stores
+
class NotFoundError < Error
- def message
+
+ ##
+ # Name that wasn't found
+
+ alias name message
+
+ def message # :nodoc:
"Nothing known about #{super}"
end
end
- attr_accessor :homepath # :nodoc:
+ attr_accessor :stores
+
+ ##
+ # Controls the user of the pager vs $stdout
+
+ attr_accessor :use_stdout
+
+ ##
+ # Default options for ri
def self.default_options
options = {}
options[:use_stdout] = !$stdout.tty?
options[:width] = 72
- options[:formatter] = RDoc::RI::Formatter.for 'plain'
options[:interactive] = false
options[:use_cache] = true
+ options[:profile] = false
# By default all standard paths are used.
options[:use_system] = true
@@ -95,27 +83,33 @@ class RDoc::RI::Driver
return options
end
- def self.process_args(argv)
+ ##
+ # Dump +data_path+ using pp
+
+ def self.dump data_path
+ require 'pp'
+
+ open data_path, 'rb' do |io|
+ pp Marshal.load(io.read)
+ end
+ end
+
+ ##
+ # Parses +argv+ and returns a Hash of options
+
+ 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
- directories = [
- RDoc::RI::Paths::SYSDIR,
- RDoc::RI::Paths::SITEDIR,
- RDoc::RI::Paths::HOMEDIR
- ]
-
- if RDoc::RI::Paths::GEMDIRS then
- Gem.path.each do |dir|
- directories << "#{dir}/doc/*/ri"
- end
- end
-
opt.banner = <<-EOT
Usage: #{opt.program_name} [options] [names...]
@@ -142,9 +136,9 @@ punctuation:
#{opt.program_name} 'Array.[]'
#{opt.program_name} compact\\!
-By default ri searches for documentation in the following directories:
+To see the default directories ri will search, run:
- #{directories.join "\n "}
+ #{opt.program_name} --list-doc-dirs
Specifying the --system, --site, --home, --gems or --doc-dir options will
limit ri to searching only the specified directories.
@@ -154,17 +148,60 @@ Options may also be set in the 'RI' environment variable.
opt.separator nil
opt.separator "Options:"
+
opt.separator nil
- opt.on("--fmt=FORMAT", "--format=FORMAT", "-f",
- RDoc::RI::Formatter::FORMATTERS.keys,
- "Format to use when displaying output:",
- " #{RDoc::RI::Formatter.list}",
- "Use 'bs' (backspace) with most pager",
- "programs. To use ANSI, either disable the",
- "pager or tell the pager to allow control",
- "characters.") do |value|
- options[:formatter] = RDoc::RI::Formatter.for value
+ 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
@@ -184,21 +221,11 @@ Options may also be set in the 'RI' environment variable.
opt.separator nil
- opt.on("--[no-]use-cache",
- "Whether or not to use ri's cache.",
- "True by default.") do |value|
- options[:use_cache] = value
- end
-
- opt.separator nil
-
opt.on("--no-standard-docs",
"Do not include documentation from",
"the Ruby standard library, site_lib,",
"installed gems, or ~/.rdoc.",
- "Equivalent to specifying",
- "the options --no-system, --no-site, --no-gems,",
- "and --no-home") do
+ "Use with --doc-dir") do
options[:use_system] = false
options[:use_site] = false
options[:use_gems] = false
@@ -239,38 +266,12 @@ Options may also be set in the 'RI' environment variable.
end
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("--no-pager", "-T",
- "Send output directly to stdout,",
- "rather than to a pager.") do
- options[:use_stdout] = true
- end
-
- opt.on("--interactive", "-i",
- "This makes ri go into interactive mode.",
- "When ri is in interactive mode it will",
- "allow the user to disambiguate lists of",
- "methods in case multiple methods match",
- "against a method search string. It also",
- "will allow the user to enter in a method",
- "name (with auto-completion, if readline",
- "is supported) when viewing a class.") do
- options[:interactive] = true
- end
-
+ opt.separator "Debug options:"
opt.separator nil
- opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger,
- "Set the width of the output.") do |value|
- options[:width] = value
+ opt.on("--dump=CACHE", File,
+ "Dumps data from an ri cache or data file") do |value|
+ options[:dump_path] = value
end
end
@@ -280,7 +281,6 @@ Options may also be set in the 'RI' environment variable.
options[:names] = argv
- options[:formatter] ||= RDoc::RI::Formatter.for('plain')
options[:use_stdout] ||= !$stdout.tty?
options[:use_stdout] ||= options[:interactive]
options[:width] ||= 72
@@ -294,376 +294,765 @@ Options may also be set in the 'RI' environment variable.
exit 1
end
- def self.run(argv = ARGV)
+ ##
+ # Runs the ri command line executable using +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 initialize(initial_options={})
+ ##
+ # Creates a new driver using +initial_options+ from ::process_args
+
+ 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]
- @class_cache_name = 'classes'
- @doc_dirs = RDoc::RI::Paths.path(options[:use_system],
- options[:use_site],
- options[:use_home],
- options[:use_gems],
- options[:extra_doc_dirs])
+ @doc_dirs = []
+ @stores = []
- @homepath = RDoc::RI::Paths.raw_path(false, false, true, false).first
- @homepath = @homepath.sub(/\.rdoc/, '.ri')
- @sys_dir = RDoc::RI::Paths.raw_path(true, false, false, false).first
- @list_doc_dirs = options[:list_doc_dirs]
+ 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
- FileUtils.mkdir_p cache_file_path unless File.directory? cache_file_path
- @cache_doc_dirs_path = File.join cache_file_path, ".doc_dirs"
+ store = RDoc::RI::Store.new path, type
+ store.load_cache
+ @stores << store
+ end
- @use_cache = options[:use_cache]
- @class_cache = nil
+ @list_doc_dirs = options[:list_doc_dirs]
@interactive = options[:interactive]
- @display = RDoc::RI::DefaultDisplay.new(options[:formatter],
- options[:width],
- options[:use_stdout])
+ @use_stdout = options[:use_stdout]
end
- def class_cache
- return @class_cache if @class_cache
+ ##
+ # Adds paths for undocumented classes +also_in+ to +out+
- # Get the documentation directories used to make the cache in order to see
- # whether the cache is valid for the current ri instantiation.
- if(File.readable?(@cache_doc_dirs_path))
- cache_doc_dirs = IO.read(@cache_doc_dirs_path).split("\n")
- else
- cache_doc_dirs = []
- end
-
- newest = map_dirs('created.rid') do |f|
- File.mtime f if test ?f, f
- end.max
-
- # An up to date cache file must have been created more recently than
- # the last modification of any of the documentation directories. It also
- # must have been created with the same documentation directories
- # as those from which ri currently is sourcing documentation.
- up_to_date = (File.exist?(class_cache_file_path) and
- newest and newest < File.mtime(class_cache_file_path) and
- (cache_doc_dirs == @doc_dirs))
-
- if up_to_date and @use_cache then
- open class_cache_file_path, 'rb' do |fp|
- begin
- @class_cache = Marshal.load fp.read
- rescue
- #
- # This shouldn't be necessary, since the up_to_date logic above
- # should force the cache to be recreated when a new version of
- # rdoc is installed. This seems like a worthwhile enhancement
- # to ri's robustness, however.
- #
- $stderr.puts "Error reading the class cache; recreating the class cache!"
- @class_cache = create_class_cache
- end
- end
- else
- @class_cache = create_class_cache
+ 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
- @class_cache
+ ##
+ # Adds a class header to +out+ for class +name+ which is described in
+ # +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 create_class_cache
- class_cache = OpenStructHash.new
+ ##
+ # Adds "(from ...)" to +out+ for +store+
- if(@use_cache)
- # Dump the documentation directories to a file in the cache, so that
- # we only will use the cache for future instantiations with identical
- # documentation directories.
- File.open @cache_doc_dirs_path, "wb" do |fp|
- fp << @doc_dirs.join("\n")
- end
- end
+ def add_from out, store
+ out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
+ end
- classes = map_dirs('**/cdesc*.yaml') { |f| Dir[f] }
- warn "Updating class cache with #{classes.size} classes..."
- populate_class_cache class_cache, classes
+ ##
+ # Adds +includes+ to +out+
- write_cache class_cache, class_cache_file_path
+ def add_includes out, includes
+ return if includes.empty?
- class_cache
- end
+ out << RDoc::Markup::Rule.new(1)
+ out << RDoc::Markup::Heading.new(1, "Includes:")
- def populate_class_cache(class_cache, classes, extension = false)
- classes.each do |cdesc|
- desc = read_yaml cdesc
- klassname = desc["full_name"]
+ 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})")
- unless class_cache.has_key? klassname then
- desc["display_name"] = "Class"
- desc["sources"] = [cdesc]
- desc["instance_method_extensions"] = []
- desc["class_method_extensions"] = []
- class_cache[klassname] = desc
+ if include.comment then
+ out << RDoc::Markup::BlankLine.new
+ out << include.comment
+ end
else
- klass = class_cache[klassname]
+ out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
+
+ wout, with = modules.partition { |incl| incl.comment.empty? }
- if extension then
- desc["instance_method_extensions"] = desc.delete "instance_methods"
- desc["class_method_extensions"] = desc.delete "class_methods"
+ 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
- klass.merge_enums desc
- klass["sources"] << cdesc
+ 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 class_cache_file_path
- File.join cache_file_path, @class_cache_name
+ ##
+ # Adds a list of +methods+ to +out+ with a heading of +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 cache_file_for(klassname)
- File.join cache_file_path, klassname.gsub(/:+/, "-")
+ ##
+ # Returns ancestor classes 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 cache_file_path
- File.join @homepath, 'cache'
+ ##
+ # For RubyGems backwards compatibility
+
+ def class_cache # :nodoc:
end
- def display_class(name)
- klass = class_cache[name]
- @display.display_class_info klass
+ ##
+ # Hash mapping a known class or module to the stores it can be loaded from
+
+ 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 display_method(method)
- @display.display_method_info method
+ ##
+ # Completes +name+ based on the caches. For Readline
+
+ 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 get_info_for(arg)
- @names = [arg]
- run
+ ##
+ # Converts +document+ to text and writes it to the pager
+
+ def display document
+ page do |io|
+ text = document.accept formatter
+
+ io.write text
+ end
end
- def load_cache_for(klassname)
- path = cache_file_for klassname
+ ##
+ # Outputs formatted RI data for class +name+. Groups undocumented classes
- cache = nil
+ def display_class name
+ return if name =~ /#|\./
- if File.exist? path and
- File.mtime(path) >= File.mtime(class_cache_file_path) and
- @use_cache then
- open path, 'rb' do |fp|
- begin
- cache = Marshal.load fp.read
- rescue
- #
- # The cache somehow is bad. Recreate the cache.
- #
- $stderr.puts "Error reading the cache for #{klassname}; recreating the cache!"
- cache = create_cache_for klassname, path
- end
+ 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
- else
- cache = create_cache_for klassname, path
+ 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
- cache
+ add_also_in out, also_in
+
+ display out
end
- def create_cache_for(klassname, path)
- klass = class_cache[klassname]
- return nil unless klass
+ ##
+ # Outputs formatted RI data for method +name+
+
+ def display_method name
+ found = load_methods_matching name
- method_files = klass["sources"]
- cache = OpenStructHash.new
+ raise NotFoundError, name if found.empty?
- method_files.each do |f|
- system_file = f.index(@sys_dir) == 0
- Dir[File.join(File.dirname(f), "*")].each do |yaml|
- next unless yaml =~ /yaml$/
- next if yaml =~ /cdesc-[^\/]+yaml$/
+ out = RDoc::Markup::Document.new
- method = read_yaml yaml
+ out << RDoc::Markup::Heading.new(1, name)
+ out << RDoc::Markup::BlankLine.new
- if system_file then
- method["source_path"] = "Ruby #{RDoc::RI::Paths::VERSION}"
- else
- if(f =~ %r%gems/[\d.]+/doc/([^/]+)%) then
- ext_path = "gem #{$1}"
- else
- ext_path = f
- end
+ 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.call_seq then
+ call_seq = method.call_seq.chomp.split "\n"
+ call_seq = call_seq.map { |line| [' ', line, "\n"] }
+ out << RDoc::Markup::Verbatim.new(*call_seq.flatten)
+ end
- method["source_path"] = ext_path
+ if method.block_params then
+ out << RDoc::Markup::BlankLine.new if method.call_seq
+ params = "yields: #{method.block_params}"
+ out << RDoc::Markup::Verbatim.new(' ', params, "\n")
end
- name = method["full_name"]
- cache[name] = method
+ out << RDoc::Markup::Rule.new(1) if
+ method.call_seq or method.block_params
+
+ out << RDoc::Markup::BlankLine.new
+ out << method.comment
+ out << RDoc::Markup::BlankLine.new
end
end
- write_cache cache, path
+ display out
+ end
+
+ ##
+ # Outputs formatted RI data for the class or method +name+.
+ #
+ # Returns true if +name+ was found, false if it was not an alternative could
+ # be guessed, raises an error if +name+ couldn't be guessed.
+
+ 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
##
- # Finds the next ancestor of +orig_klass+ after +klass+.
+ # Displays each name in +name+
+
+ def display_names names
+ names.each do |name|
+ name = expand_name name
- def lookup_ancestor(klass, orig_klass)
- # This is a bit hacky, but ri will go into an infinite
- # loop otherwise, since Object has an Object ancestor
- # for some reason. Depending on the documentation state, I've seen
- # Kernel as an ancestor of Object and not as an ancestor of Object.
- if ((orig_klass == "Object") &&
- ((klass == "Kernel") || (klass == "Object")))
- return nil
+ display_name name
end
+ end
+ ##
+ # Expands abbreviated klass +klass+ into a fully-qualified class. "Zl::Da"
+ # will be expanded to Zlib::DataError.
+
+ def expand_class klass
+ klass.split('::').inject '' do |expanded, klass_part|
+ expanded << '::' unless expanded.empty?
+ short = expanded << klass_part
- cache = class_cache[orig_klass]
+ subset = classes.keys.select do |klass_name|
+ klass_name =~ /^#{expanded}[^:]*$/
+ end
- return nil unless cache
+ abbrevs = Abbrev.abbrev subset
- ancestors = [orig_klass]
- ancestors.push(*cache.includes.map { |inc| inc['name'] })
- ancestors << cache.superclass
+ expanded = abbrevs[short]
- ancestor_index = ancestors.index(klass)
+ raise NotFoundError, short unless expanded
- if ancestor_index
- ancestor = ancestors[ancestors.index(klass) + 1]
- return ancestor if ancestor
+ expanded.dup
end
+ end
+
+ ##
+ # Expands the class portion of +name+ into a fully-qualified class. See
+ # #expand_class.
+
+ def expand_name name
+ klass, selector, method = parse_name name
+
+ return [selector, method].join if klass.empty?
- lookup_ancestor klass, cache.superclass
+ "#{expand_class klass}#{selector}#{method}"
end
##
- # Finds the method
+ # Yields items matching +name+ including the store they were found in, the
+ # class being searched for, the class they were found in (an ancestor) the
+ # types of methods to look up (from #method_type), and the method name being
+ # searched for
+
+ 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
- def lookup_method(name, klass)
- cache = load_cache_for klass
- return nil unless cache
+ methods.each do |item|
+ yield(*item)
+ end
- method = cache[name.gsub('.', '#')]
- method = cache[name.gsub('.', '::')] unless method
- method
+ self
end
- def map_dirs(file_name)
- @doc_dirs.map { |dir| yield File.join(dir, file_name) }.flatten.compact
+ ##
+ # Creates a new RDoc::Markup::Formatter. If a formatter is given with -f,
+ # use it. If we're outputting to a pager, use bs, otherwise ansi.
+
+ def formatter
+ if @formatter_klass then
+ @formatter_klass.new
+ elsif paging? then
+ RDoc::Markup::ToBs.new
+ else
+ RDoc::Markup::ToAnsi.new
+ end
end
##
- # Extract the class and method name parts from +name+ like Foo::Bar#baz
+ # Runs ri interactively using Readline if it is available.
- def parse_name(name)
- parts = name.split(/(::|\#|\.)/)
+ def interactive
+ puts "\nEnter the method name you want to look up."
- if parts[-2] != '::' or parts.last !~ /^[A-Z]/ then
- meth = parts.pop
- parts.pop
+ if defined? Readline then
+ Readline.completion_proc = method :complete
+ puts "You can use tab to autocomplete."
end
- klass = parts.join
+ puts "Enter a blank line to exit.\n\n"
- [klass, meth]
- end
+ loop do
+ name = if defined? Readline then
+ Readline.readline ">> "
+ else
+ print ">> "
+ $stdin.gets
+ end
+
+ return if name.nil? or name.empty?
- def read_yaml(path)
- data = File.read path
+ name = expand_name name.strip
+
+ begin
+ display_name name
+ rescue NotFoundError => e
+ puts e.message
+ end
+ end
- # Necessary to be backward-compatible with documentation generated
- # by earliar RDoc versions.
- data = data.gsub(/ \!ruby\/(object|struct):(RDoc::RI|RI).*/, '')
- data = data.gsub(/ \!ruby\/(object|struct):SM::(\S+)/,
- ' !ruby/\1:RDoc::Markup::\2')
- OpenStructHash.convert(YAML.load(data))
+ rescue Interrupt
+ exit
end
- def run
- if(@list_doc_dirs)
- puts @doc_dirs.join("\n")
- elsif @names.empty? then
- @display.list_known_classes class_cache.keys.sort
- else
- @names.each do |name|
- if class_cache.key? name then
- method_map = display_class name
- if(@interactive)
- method_name = @display.get_class_method_choice(method_map)
-
- if(method_name != nil)
- method = lookup_method "#{name}#{method_name}", name
- display_method method
- end
- end
- elsif name =~ /::|\#|\./ then
- klass, = parse_name name
+ ##
+ # Lists classes known to ri
- orig_klass = klass
- orig_name = name
+ def list_known_classes
+ classes = []
- loop do
- method = lookup_method name, klass
+ stores.each do |store|
+ classes << store.modules
+ end
- break method if method
+ classes = classes.flatten.uniq.sort
- ancestor = lookup_ancestor klass, orig_klass
+ page do |io|
+ if paging? or io.tty? then
+ io.puts "Classes and Modules known to ri:"
+ io.puts
+ end
- break unless ancestor
+ io.puts classes.join("\n")
+ end
+ end
- name = name.sub klass, ancestor
- klass = ancestor
- end
+ ##
+ # Returns an Array of 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]
- raise NotFoundError, orig_name unless method
-
- display_method method
- else
- methods = select_methods(/#{name}/)
-
- if methods.size == 0
- raise NotFoundError, name
- elsif methods.size == 1
- display_method methods[0]
- else
- if(@interactive)
- @display.display_method_list_choice methods
- else
- @display.display_method_list methods
- end
+ 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
- rescue NotFoundError => e
- abort e.message
+
+ found.uniq
end
- def select_methods(pattern)
- methods = []
- class_cache.keys.sort.each do |klass|
- class_cache[klass]["instance_methods"].map{|h|h["name"]}.grep(pattern) do |name|
- method = load_cache_for(klass)[klass+'#'+name]
- methods << method if method
- end
- class_cache[klass]["class_methods"].map{|h|h["name"]}.grep(pattern) do |name|
- method = load_cache_for(klass)[klass+'::'+name]
- methods << method if method
+ ##
+ # Loads RI data for method +name+ on +klass+ from +store+. +type+ and
+ # +cache+ indicate if it is a class or instance method.
+
+ 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
+
+ ##
+ # Returns an Array of RI data for 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
+
+ ##
+ # Returns the type of method (:both, :instance, :class) for +selector+
+
+ def method_type selector
+ case selector
+ when '.', nil then :both
+ when '#' then :instance
+ else :class
+ end
+ end
+
+ ##
+ # Paginates output through a pager program.
+
+ def page
+ if pager = setup_pager then
+ begin
+ yield pager
+ ensure
+ pager.close
end
+ else
+ yield $stdout
end
- methods
+ rescue Errno::EPIPE
+ ensure
+ @paging = false
+ end
+
+ ##
+ # Are we using a pager?
+
+ def paging?
+ @paging
end
- def write_cache(cache, path)
- if(@use_cache)
- File.open path, "wb" do |cache_file|
- Marshal.dump cache, cache_file
+ ##
+ # Extract the class, selector and method name parts from +name+ like
+ # Foo::Bar#baz.
+ #
+ # NOTE: Given Foo::Bar, Bar is considered a class even though it may be a
+ # method
+
+ 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
+
+ ##
+ # Looks up and displays ri data according to the options given.
+
+ 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
+
+ ##
+ # Sets up a pager program to pass output through. Tries the RI_PAGER and
+ # PAGER environment variables followed by pager, less then more.
+
+ def setup_pager
+ return if @use_stdout
+
+ pagers = [ENV['RI_PAGER'], ENV['PAGER'], 'pager', 'less', 'more']
+
+ pagers.compact.uniq.each do |pager|
+ io = IO.popen(pager, "w") rescue next
+
+ @paging = true
+
+ return io
end
- cache
+ @use_stdout = true
+
+ nil
end
end
+