summaryrefslogtreecommitdiff
path: root/lib/rdoc/ri
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rdoc/ri')
-rw-r--r--lib/rdoc/ri/cache.rb187
-rw-r--r--lib/rdoc/ri/descriptions.rb156
-rw-r--r--lib/rdoc/ri/display.rb392
-rw-r--r--lib/rdoc/ri/driver.rb1155
-rw-r--r--lib/rdoc/ri/formatter.rb615
-rw-r--r--lib/rdoc/ri/gemdirs.rb28
-rw-r--r--lib/rdoc/ri/paths.rb148
-rw-r--r--lib/rdoc/ri/reader.rb106
-rw-r--r--lib/rdoc/ri/store.rb248
-rw-r--r--lib/rdoc/ri/util.rb79
-rw-r--r--lib/rdoc/ri/writer.rb68
11 files changed, 1116 insertions, 2066 deletions
diff --git a/lib/rdoc/ri/cache.rb b/lib/rdoc/ri/cache.rb
deleted file mode 100644
index 06177a00de..0000000000
--- a/lib/rdoc/ri/cache.rb
+++ /dev/null
@@ -1,187 +0,0 @@
-require 'rdoc/ri'
-
-class RDoc::RI::ClassEntry
-
- attr_reader :name
- attr_reader :path_names
-
- def initialize(path_name, name, in_class)
- @path_names = [ path_name ]
- @name = name
- @in_class = in_class
- @class_methods = []
- @instance_methods = []
- @inferior_classes = []
- end
-
- # We found this class in more than one place, so add
- # in the name from there.
- def add_path(path)
- @path_names << path
- end
-
- # read in our methods and any classes
- # and modules in our namespace. Methods are
- # stored in files called name-c|i.yaml,
- # where the 'name' portion is the external
- # form of the method name and the c|i is a class|instance
- # flag
-
- def load_from(dir)
- Dir.foreach(dir) do |name|
- next if name =~ /^\./
-
- # convert from external to internal form, and
- # extract the instance/class flag
-
- if name =~ /^(.*?)-(c|i).yaml$/
- external_name = $1
- is_class_method = $2 == "c"
- internal_name = RDoc::RI::Writer.external_to_internal(external_name)
- list = is_class_method ? @class_methods : @instance_methods
- path = File.join(dir, name)
- list << RDoc::RI::MethodEntry.new(path, internal_name, is_class_method, self)
- else
- full_name = File.join(dir, name)
- if File.directory?(full_name)
- inf_class = @inferior_classes.find {|c| c.name == name }
- if inf_class
- inf_class.add_path(full_name)
- else
- inf_class = RDoc::RI::ClassEntry.new(full_name, name, self)
- @inferior_classes << inf_class
- end
- inf_class.load_from(full_name)
- end
- end
- end
- end
-
- # Return a list of any classes or modules that we contain
- # that match a given string
-
- def contained_modules_matching(name)
- @inferior_classes.find_all {|c| c.name[name]}
- end
-
- def classes_and_modules
- @inferior_classes
- end
-
- # Return an exact match to a particular name
- def contained_class_named(name)
- @inferior_classes.find {|c| c.name == name}
- end
-
- # return the list of local methods matching name
- # We're split into two because we need distinct behavior
- # when called from the _toplevel_
- def methods_matching(name, is_class_method)
- local_methods_matching(name, is_class_method)
- end
-
- # Find methods matching 'name' in ourselves and in
- # any classes we contain
- def recursively_find_methods_matching(name, is_class_method)
- res = local_methods_matching(name, is_class_method)
- @inferior_classes.each do |c|
- res.concat(c.recursively_find_methods_matching(name, is_class_method))
- end
- res
- end
-
-
- # Return our full name
- def full_name
- res = @in_class.full_name
- res << "::" unless res.empty?
- res << @name
- end
-
- # Return a list of all out method names
- def all_method_names
- res = @class_methods.map {|m| m.full_name }
- @instance_methods.each {|m| res << m.full_name}
- res
- end
-
- private
-
- # Return a list of all our methods matching a given string.
- # Is +is_class_methods+ if 'nil', we don't care if the method
- # is a class method or not, otherwise we only return
- # those methods that match
- def local_methods_matching(name, is_class_method)
-
- list = case is_class_method
- when nil then @class_methods + @instance_methods
- when true then @class_methods
- when false then @instance_methods
- else fail "Unknown is_class_method: #{is_class_method.inspect}"
- end
-
- list.find_all {|m| m.name; m.name[name]}
- end
-end
-
-##
-# A TopLevelEntry is like a class entry, but when asked to search for methods
-# searches all classes, not just itself
-
-class RDoc::RI::TopLevelEntry < RDoc::RI::ClassEntry
- def methods_matching(name, is_class_method)
- res = recursively_find_methods_matching(name, is_class_method)
- end
-
- def full_name
- ""
- end
-
- def module_named(name)
-
- end
-
-end
-
-class RDoc::RI::MethodEntry
- attr_reader :name
- attr_reader :path_name
-
- def initialize(path_name, name, is_class_method, in_class)
- @path_name = path_name
- @name = name
- @is_class_method = is_class_method
- @in_class = in_class
- end
-
- def full_name
- res = @in_class.full_name
- unless res.empty?
- if @is_class_method
- res << "::"
- else
- res << "#"
- end
- end
- res << @name
- end
-end
-
-##
-# We represent everything known about all 'ri' files accessible to this program
-
-class RDoc::RI::Cache
-
- attr_reader :toplevel
-
- def initialize(dirs)
- # At the top level we have a dummy module holding the
- # overall namespace
- @toplevel = RDoc::RI::TopLevelEntry.new('', '::', nil)
-
- dirs.each do |dir|
- @toplevel.load_from(dir)
- end
- end
-
-end
diff --git a/lib/rdoc/ri/descriptions.rb b/lib/rdoc/ri/descriptions.rb
deleted file mode 100644
index 467b7de2a9..0000000000
--- a/lib/rdoc/ri/descriptions.rb
+++ /dev/null
@@ -1,156 +0,0 @@
-require 'yaml'
-require 'rdoc/markup/fragments'
-require 'rdoc/ri'
-
-##
-# Descriptions are created by RDoc (in ri_generator) and written out in
-# serialized form into the documentation tree. ri then reads these to generate
-# the documentation
-
-class RDoc::RI::NamedThing
- attr_reader :name
- def initialize(name)
- @name = name
- end
-
- def <=>(other)
- @name <=> other.name
- end
-
- def hash
- @name.hash
- end
-
- def eql?(other)
- @name.eql?(other)
- end
-end
-
-class RDoc::RI::AliasName < RDoc::RI::NamedThing; end
-
-class RDoc::RI::Attribute < RDoc::RI::NamedThing
- attr_reader :rw, :comment
-
- def initialize(name, rw, comment)
- super(name)
- @rw = rw
- @comment = comment
- end
-end
-
-class RDoc::RI::Constant < RDoc::RI::NamedThing
- attr_reader :value, :comment
-
- def initialize(name, value, comment)
- super(name)
- @value = value
- @comment = comment
- end
-end
-
-class RDoc::RI::IncludedModule < RDoc::RI::NamedThing; end
-
-class RDoc::RI::MethodSummary < RDoc::RI::NamedThing
- def initialize(name="")
- super
- end
-end
-
-class RDoc::RI::Description
- attr_accessor :name
- attr_accessor :full_name
- attr_accessor :comment
-
- def serialize
- self.to_yaml
- end
-
- def self.deserialize(from)
- YAML.load(from)
- end
-
- def <=>(other)
- @name <=> other.name
- end
-end
-
-class RDoc::RI::ModuleDescription < RDoc::RI::Description
-
- attr_accessor :class_methods
- attr_accessor :class_method_extensions
- attr_accessor :instance_methods
- attr_accessor :instance_method_extensions
- attr_accessor :attributes
- attr_accessor :constants
- attr_accessor :includes
-
- # merge in another class description into this one
- def merge_in(old)
- merge(@class_methods, old.class_methods)
- merge(@instance_methods, old.instance_methods)
- merge(@attributes, old.attributes)
- merge(@constants, old.constants)
- merge(@includes, old.includes)
- if @comment.nil? || @comment.empty?
- @comment = old.comment
- else
- unless old.comment.nil? or old.comment.empty? then
- if @comment.nil? or @comment.empty? then
- @comment = old.comment
- else
- @comment << RDoc::Markup::Flow::RULE.new
- @comment.concat old.comment
- end
- end
- end
- end
-
- def display_name
- "Module"
- end
-
- # the 'ClassDescription' subclass overrides this
- # to format up the name of a parent
- def superclass_string
- nil
- end
-
- private
-
- def merge(into, from)
- names = {}
- into.each {|i| names[i.name] = i }
- from.each {|i| names[i.name] = i }
- into.replace(names.keys.sort.map {|n| names[n]})
- end
-end
-
-class RDoc::RI::ClassDescription < RDoc::RI::ModuleDescription
- attr_accessor :superclass
-
- def display_name
- "Class"
- end
-
- def superclass_string
- if @superclass && @superclass != "Object"
- @superclass
- else
- nil
- end
- end
-end
-
-class RDoc::RI::MethodDescription < RDoc::RI::Description
-
- attr_accessor :is_class_method
- attr_accessor :visibility
- attr_accessor :block_params
- attr_accessor :is_singleton
- attr_accessor :aliases
- attr_accessor :is_alias_for
- attr_accessor :params
- attr_accessor :source_path
-
-end
-
diff --git a/lib/rdoc/ri/display.rb b/lib/rdoc/ri/display.rb
deleted file mode 100644
index f6b647fbc1..0000000000
--- a/lib/rdoc/ri/display.rb
+++ /dev/null
@@ -1,392 +0,0 @@
-require 'rdoc/ri'
-
-# readline support might not be present, so be careful
-# when requiring it.
-begin
- require('readline')
- require('abbrev')
- CAN_USE_READLINE = true # HACK use an RDoc namespace constant
-rescue LoadError
- CAN_USE_READLINE = false
-end
-
-##
-# This is a kind of 'flag' module. If you want to write your own 'ri' display
-# module (perhaps because you're writing an IDE), you write a class which
-# implements the various 'display' methods in RDoc::RI::DefaultDisplay, and
-# include the RDoc::RI::Display module in that class.
-#
-# To access your class from the command line, you can do
-#
-# ruby -r <your source file> ../ri ....
-
-module RDoc::RI::Display
-
- @@display_class = nil
-
- def self.append_features(display_class)
- @@display_class = display_class
- end
-
- def self.new(*args)
- @@display_class.new(*args)
- end
-
-end
-
-##
-# A paging display module. Uses the RDoc::RI::Formatter class to do the actual
-# presentation.
-
-class RDoc::RI::DefaultDisplay
-
- include RDoc::RI::Display
-
- def initialize(formatter, width, use_stdout, output = $stdout)
- @use_stdout = use_stdout
- @formatter = formatter.new output, width, " "
- end
-
- ##
- # Display information about +klass+. Fetches additional information from
- # +ri_reader+ as necessary.
-
- def display_class_info(klass)
- page do
- superclass = klass.superclass
-
- if superclass
- superclass = " < " + superclass
- else
- superclass = ""
- end
-
- @formatter.draw_line(klass.display_name + ": " +
- klass.full_name + superclass)
-
- display_flow(klass.comment)
- @formatter.draw_line
-
- unless klass.includes.empty?
- @formatter.blankline
- @formatter.display_heading("Includes:", 2, "")
- incs = []
-
- klass.includes.each do |inc|
- incs << inc.name
- end
-
- @formatter.wrap(incs.sort.join(', '))
- end
-
- unless klass.constants.empty?
- @formatter.blankline
- @formatter.display_heading("Constants:", 2, "")
-
- constants = klass.constants.sort_by { |constant| constant.name }
-
- constants.each do |constant|
- @formatter.wrap "#{constant.name} = #{constant.value}"
- if constant.comment then
- @formatter.indent do
- @formatter.display_flow constant.comment
- end
- else
- @formatter.break_to_newline
- end
- end
- end
-
- unless klass.attributes.empty? then
- @formatter.blankline
- @formatter.display_heading 'Attributes:', 2, ''
-
- attributes = klass.attributes.sort_by { |attribute| attribute.name }
-
- attributes.each do |attribute|
- if attribute.comment then
- @formatter.wrap "#{attribute.name} (#{attribute.rw}):"
- @formatter.indent do
- @formatter.display_flow attribute.comment
- end
- else
- @formatter.wrap "#{attribute.name} (#{attribute.rw})"
- @formatter.break_to_newline
- end
- end
- end
-
- return display_class_method_list(klass)
- end
- end
-
- ##
- # Given a Hash mapping a class' methods to method types (returned by
- # display_class_method_list), this method allows the user to
- # choose one of the methods.
-
- def get_class_method_choice(method_map)
- if CAN_USE_READLINE
- # prepare abbreviations for tab completion
- abbreviations = method_map.keys.abbrev
- Readline.completion_proc = proc do |string|
- abbreviations.values.uniq.grep(/^#{string}/)
- end
- end
-
- @formatter.raw_print_line "\nEnter the method name you want.\n"
- @formatter.raw_print_line "Class methods can be preceeded by '::' and instance methods by '#'.\n"
-
- if CAN_USE_READLINE
- @formatter.raw_print_line "You can use tab to autocomplete.\n"
- @formatter.raw_print_line "Enter a blank line to exit.\n"
-
- choice_string = Readline.readline(">> ").strip
- else
- @formatter.raw_print_line "Enter a blank line to exit.\n"
- @formatter.raw_print_line ">> "
- choice_string = $stdin.gets.strip
- end
-
- if choice_string == ''
- return nil
- else
- class_or_instance = method_map[choice_string]
-
- if class_or_instance
- # If the user's choice is not preceeded by a '::' or a '#', figure
- # out whether they want a class or an instance method and decorate
- # the choice appropriately.
- if(choice_string =~ /^[a-zA-Z]/)
- if(class_or_instance == :class)
- choice_string = "::#{choice_string}"
- else
- choice_string = "##{choice_string}"
- end
- end
-
- return choice_string
- else
- @formatter.raw_print_line "No method matched '#{choice_string}'.\n"
- return nil
- end
- end
- end
-
-
- ##
- # Display methods on +klass+
- # Returns a hash mapping method name to method contents (HACK?)
-
- def display_class_method_list(klass)
- method_map = {}
-
- class_data = [
- :class_methods,
- :class_method_extensions,
- :instance_methods,
- :instance_method_extensions,
- ]
-
- class_data.each do |data_type|
- data = klass.send data_type
-
- unless data.nil? or data.empty? then
- @formatter.blankline
-
- heading = data_type.to_s.split('_').join(' ').capitalize << ':'
- @formatter.display_heading heading, 2, ''
-
- method_names = []
- data.each do |item|
- method_names << item.name
-
- if(data_type == :class_methods ||
- data_type == :class_method_extensions) then
- method_map["::#{item.name}"] = :class
- method_map[item.name] = :class
- else
- #
- # Since we iterate over instance methods after class methods,
- # an instance method always will overwrite the unqualified
- # class method entry for a class method of the same name.
- #
- method_map["##{item.name}"] = :instance
- method_map[item.name] = :instance
- end
- end
- method_names.sort!
-
- @formatter.wrap method_names.join(', ')
- end
- end
-
- method_map
- end
- private :display_class_method_list
-
- ##
- # Display an Array of RDoc::Markup::Flow objects, +flow+.
-
- def display_flow(flow)
- if flow and not flow.empty? then
- @formatter.display_flow flow
- else
- @formatter.wrap '[no description]'
- end
- end
-
- ##
- # Display information about +method+.
-
- def display_method_info(method)
- page do
- @formatter.draw_line(method.full_name)
- display_params(method)
-
- @formatter.draw_line
- display_flow(method.comment)
-
- if method.aliases and not method.aliases.empty? then
- @formatter.blankline
- aka = "(also known as #{method.aliases.map { |a| a.name }.join(', ')})"
- @formatter.wrap aka
- end
- end
- end
-
- ##
- # Display the list of +methods+.
-
- def display_method_list(methods)
- page do
- @formatter.wrap "More than one method matched your request. You can refine your search by asking for information on one of:"
- @formatter.blankline
-
- methods.each do |method|
- @formatter.raw_print_line "#{method.full_name} [#{method.source_path}]\n"
- end
- end
- end
-
- ##
- # Display a list of +methods+ and allow the user to select one of them.
-
- def display_method_list_choice(methods)
- page do
- @formatter.wrap "More than one method matched your request. Please choose one of the possible matches."
- @formatter.blankline
-
- methods.each_with_index do |method, index|
- @formatter.raw_print_line "%3d %s [%s]\n" % [index + 1, method.full_name, method.source_path]
- end
-
- @formatter.raw_print_line ">> "
-
- choice = $stdin.gets.strip!
-
- if(choice == '')
- return
- end
-
- choice = choice.to_i
-
- if ((choice == 0) || (choice > methods.size)) then
- @formatter.raw_print_line "Invalid choice!\n"
- else
- method = methods[choice - 1]
- display_method_info(method)
- end
- end
- end
-
- ##
- # Display the params for +method+.
-
- def display_params(method)
- params = method.params
-
- if params[0,1] == "(" then
- if method.is_singleton
- params = method.full_name + params
- else
- params = method.name + params
- end
- end
-
- params.split(/\n/).each do |param|
- @formatter.wrap param
- @formatter.break_to_newline
- end
-
- @formatter.blankline
- @formatter.wrap("From #{method.source_path}")
- end
-
- ##
- # List the classes in +classes+.
-
- def list_known_classes(classes)
- if classes.empty?
- warn_no_database
- else
- page do
- @formatter.draw_line "Known classes and modules"
- @formatter.blankline
-
- @formatter.wrap classes.sort.join(', ')
- end
- end
- end
-
- ##
- # Paginates output through a pager program.
-
- def page
- if pager = setup_pager then
- begin
- orig_output = @formatter.output
- @formatter.output = pager
- yield
- ensure
- @formatter.output = orig_output
- pager.close
- end
- else
- yield
- end
- rescue Errno::EPIPE
- end
-
- ##
- # Sets up a pager program to pass output through.
-
- def setup_pager
- unless @use_stdout then
- for pager in [ ENV['PAGER'], "less", "more", 'pager' ].compact.uniq
- return IO.popen(pager, "w") rescue nil
- end
- @use_stdout = true
- nil
- end
- end
-
- ##
- # Displays a message that describes how to build RI data.
-
- def warn_no_database
- output = @formatter.output
-
- output.puts "No ri data found"
- output.puts
- output.puts "If you've installed Ruby yourself, you need to generate documentation using:"
- output.puts
- output.puts " make install-doc"
- output.puts
- output.puts "from the same place you ran `make` to build ruby."
- output.puts
- output.puts "If you installed Ruby from a packaging system, then you may need to"
- output.puts "install an additional package, or ask the packager to enable ri generation."
- end
-
-end
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
+
diff --git a/lib/rdoc/ri/formatter.rb b/lib/rdoc/ri/formatter.rb
index 258907d141..84d37a9d31 100644
--- a/lib/rdoc/ri/formatter.rb
+++ b/lib/rdoc/ri/formatter.rb
@@ -1,616 +1,5 @@
-require 'rdoc/ri'
-require 'rdoc/markup'
-
-class RDoc::RI::Formatter
-
- attr_writer :indent
- attr_accessor :output
-
- FORMATTERS = { }
-
- def self.for(name)
- FORMATTERS[name.downcase]
- end
-
- def self.list
- FORMATTERS.keys.sort.join ", "
- end
-
- def initialize(output, width, indent)
- @output = output
- @width = width
- @indent = indent
- @original_indent = indent.dup
- end
-
- def draw_line(label=nil)
- len = @width
- len -= (label.size + 1) if label
-
- if len > 0 then
- @output.print '-' * len
- if label
- @output.print ' '
- bold_print label
- end
-
- @output.puts
- else
- @output.print '-' * @width
- @output.puts
-
- @output.puts label
- end
- end
-
- def indent
- return @indent unless block_given?
-
- begin
- indent = @indent.dup
- @indent += @original_indent
- yield
- ensure
- @indent = indent
- end
- end
-
- def wrap(txt, prefix=@indent, linelen=@width)
- return unless txt && !txt.empty?
-
- work = conv_markup(txt)
- textLen = linelen - prefix.length
- patt = Regexp.new("^(.{0,#{textLen}})[ \n]")
- next_prefix = prefix.tr("^ ", " ")
-
- res = []
-
- while work.length > textLen
- if work =~ patt
- res << $1
- work.slice!(0, $&.length)
- else
- res << work.slice!(0, textLen)
- end
- end
- res << work if work.length.nonzero?
- @output.puts(prefix + res.join("\n" + next_prefix))
- end
-
- def blankline
- @output.puts
- end
-
- ##
- # Called when we want to ensure a new 'wrap' starts on a newline. Only
- # needed for HtmlFormatter, because the rest do their own line breaking.
-
- def break_to_newline
- end
-
- def bold_print(txt)
- @output.print txt
- end
-
- def raw_print_line(txt)
- @output.print txt
- end
-
- ##
- # Convert HTML entities back to ASCII
-
- def conv_html(txt)
- txt = txt.gsub(/&gt;/, '>')
- txt.gsub!(/&lt;/, '<')
- txt.gsub!(/&quot;/, '"')
- txt.gsub!(/&amp;/, '&')
- txt
- end
-
- ##
- # Convert markup into display form
-
- def conv_markup(txt)
- txt = txt.gsub(%r{<tt>(.*?)</tt>}, '+\1+')
- txt.gsub!(%r{<code>(.*?)</code>}, '+\1+')
- txt.gsub!(%r{<b>(.*?)</b>}, '*\1*')
- txt.gsub!(%r{<em>(.*?)</em>}, '_\1_')
- txt
- end
-
- def display_list(list)
- case list.type
- when :BULLET
- prefixer = proc { |ignored| @indent + "* " }
-
- when :NUMBER, :UPPERALPHA, :LOWERALPHA then
- start = case list.type
- when :NUMBER then 1
- when :UPPERALPHA then 'A'
- when :LOWERALPHA then 'a'
- end
-
- prefixer = proc do |ignored|
- res = @indent + "#{start}.".ljust(4)
- start = start.succ
- res
- end
-
- when :LABELED, :NOTE then
- longest = 0
-
- list.contents.each do |item|
- if RDoc::Markup::Flow::LI === item and item.label.length > longest then
- longest = item.label.length
- end
- end
-
- longest += 1
-
- prefixer = proc { |li| @indent + li.label.ljust(longest) }
-
- else
- raise ArgumentError, "unknown list type #{list.type}"
- end
-
- list.contents.each do |item|
- if RDoc::Markup::Flow::LI === item then
- prefix = prefixer.call item
- display_flow_item item, prefix
- else
- display_flow_item item
- end
- end
- end
-
- def display_flow_item(item, prefix = @indent)
- case item
- when RDoc::Markup::Flow::P, RDoc::Markup::Flow::LI
- wrap(conv_html(item.body), prefix)
- blankline
-
- when RDoc::Markup::Flow::LIST
- display_list(item)
-
- when RDoc::Markup::Flow::VERB
- display_verbatim_flow_item(item, @indent)
-
- when RDoc::Markup::Flow::H
- display_heading(conv_html(item.text), item.level, @indent)
-
- when RDoc::Markup::Flow::RULE
- draw_line
-
- else
- raise RDoc::Error, "Unknown flow element: #{item.class}"
- end
- end
-
- def display_verbatim_flow_item(item, prefix=@indent)
- item.body.split(/\n/).each do |line|
- @output.print @indent, conv_html(line), "\n"
- end
- blankline
- end
-
- def display_heading(text, level, indent)
- text = strip_attributes text
-
- case level
- when 1 then
- ul = "=" * text.length
- @output.puts
- @output.puts text.upcase
- @output.puts ul
-
- when 2 then
- ul = "-" * text.length
- @output.puts
- @output.puts text
- @output.puts ul
- else
- @output.print indent, text, "\n"
- end
-
- @output.puts
- end
-
- def display_flow(flow)
- flow.each do |f|
- display_flow_item(f)
- end
- end
-
- def strip_attributes(text)
- text.gsub(/(<\/?(?:b|code|em|i|tt)>)/, '')
- end
-
-end
-
-##
-# Handle text with attributes. We're a base class: there are different
-# presentation classes (one, for example, uses overstrikes to handle bold and
-# underlining, while another using ANSI escape sequences.
-
-class RDoc::RI::AttributeFormatter < RDoc::RI::Formatter
-
- BOLD = 1
- ITALIC = 2
- CODE = 4
-
- ATTR_MAP = {
- "b" => BOLD,
- "code" => CODE,
- "em" => ITALIC,
- "i" => ITALIC,
- "tt" => CODE
- }
-
- AttrChar = Struct.new :char, :attr
-
- class AttributeString
- attr_reader :txt
-
- def initialize
- @txt = []
- @optr = 0
- end
-
- def <<(char)
- @txt << char
- end
-
- def empty?
- @optr >= @txt.length
- end
-
- # accept non space, then all following spaces
- def next_word
- start = @optr
- len = @txt.length
-
- while @optr < len && @txt[@optr].char != " "
- @optr += 1
- end
-
- while @optr < len && @txt[@optr].char == " "
- @optr += 1
- end
-
- @txt[start...@optr]
- end
- end
-
- ##
- # Overrides base class. Looks for <tt>...</tt> etc sequences and generates
- # an array of AttrChars. This array is then used as the basis for the
- # split.
-
- def wrap(txt, prefix=@indent, linelen=@width)
- return unless txt && !txt.empty?
-
- txt = add_attributes_to(txt)
- next_prefix = prefix.tr("^ ", " ")
- linelen -= prefix.size
-
- line = []
-
- until txt.empty?
- word = txt.next_word
- if word.size + line.size > linelen
- write_attribute_text(prefix, line)
- prefix = next_prefix
- line = []
- end
- line.concat(word)
- end
-
- write_attribute_text(prefix, line) if line.length > 0
- end
-
- protected
-
- def write_attribute_text(prefix, line)
- @output.print prefix
- line.each do |achar|
- @output.print achar.char
- end
- @output.puts
- end
-
- def bold_print(txt)
- @output.print txt
- end
-
- private
-
- def add_attributes_to(txt)
- tokens = txt.split(%r{(</?(?:b|code|em|i|tt)>)})
- text = AttributeString.new
- attributes = 0
- tokens.each do |tok|
- case tok
- when %r{^</(\w+)>$} then attributes &= ~(ATTR_MAP[$1]||0)
- when %r{^<(\w+)>$} then attributes |= (ATTR_MAP[$1]||0)
- else
- tok.split(//).each {|ch| text << AttrChar.new(ch, attributes)}
- end
- end
- text
- end
-
-end
-
-##
-# This formatter generates overstrike-style formatting, which works with
-# pagers such as man and less.
-
-class RDoc::RI::OverstrikeFormatter < RDoc::RI::AttributeFormatter
-
- BS = "\C-h"
-
- def write_attribute_text(prefix, line)
- @output.print prefix
-
- line.each do |achar|
- attr = achar.attr
- @output.print "_", BS if (attr & (ITALIC + CODE)) != 0
- @output.print achar.char, BS if (attr & BOLD) != 0
- @output.print achar.char
- end
-
- @output.puts
- end
-
- ##
- # Draw a string in bold
-
- def bold_print(text)
- text.split(//).each do |ch|
- @output.print ch, BS, ch
- end
- end
-
-end
-
-##
-# This formatter uses ANSI escape sequences to colorize stuff works with
-# pagers such as man and less.
-
-class RDoc::RI::AnsiFormatter < RDoc::RI::AttributeFormatter
-
- def initialize(*args)
- super
- @output.print "\033[0m"
- end
-
- def write_attribute_text(prefix, line)
- @output.print prefix
- curr_attr = 0
- line.each do |achar|
- attr = achar.attr
- if achar.attr != curr_attr
- update_attributes(achar.attr)
- curr_attr = achar.attr
- end
- @output.print achar.char
- end
- update_attributes(0) unless curr_attr.zero?
- @output.puts
- end
-
- def bold_print(txt)
- @output.print "\033[1m#{txt}\033[m"
- end
-
- HEADINGS = {
- 1 => ["\033[1;32m", "\033[m"],
- 2 => ["\033[4;32m", "\033[m"],
- 3 => ["\033[32m", "\033[m"],
- }
-
- def display_heading(text, level, indent)
- level = 3 if level > 3
- heading = HEADINGS[level]
- @output.print indent
- @output.print heading[0]
- @output.print strip_attributes(text)
- @output.puts heading[1]
- end
-
- private
-
- ATTR_MAP = {
- BOLD => "1",
- ITALIC => "33",
- CODE => "36"
- }
-
- def update_attributes(attr)
- str = "\033["
- for quality in [ BOLD, ITALIC, CODE]
- unless (attr & quality).zero?
- str << ATTR_MAP[quality]
- end
- end
- @output.print str, "m"
- end
-
-end
-
##
-# This formatter uses HTML.
-
-class RDoc::RI::HtmlFormatter < RDoc::RI::AttributeFormatter
-
- def write_attribute_text(prefix, line)
- curr_attr = 0
- line.each do |achar|
- attr = achar.attr
- if achar.attr != curr_attr
- update_attributes(curr_attr, achar.attr)
- curr_attr = achar.attr
- end
- @output.print(escape(achar.char))
- end
- update_attributes(curr_attr, 0) unless curr_attr.zero?
- end
-
- def draw_line(label=nil)
- if label != nil
- bold_print(label)
- end
- @output.puts("<hr>")
- end
-
- def bold_print(txt)
- tag("b") { txt }
- end
-
- def blankline()
- @output.puts("<p>")
- end
-
- def break_to_newline
- @output.puts("<br>")
- end
-
- def display_heading(text, level, indent)
- level = 4 if level > 4
- tag("h#{level}") { text }
- @output.puts
- end
-
- def display_list(list)
- case list.type
- when :BULLET then
- list_type = "ul"
- prefixer = proc { |ignored| "<li>" }
-
- when :NUMBER, :UPPERALPHA, :LOWERALPHA then
- list_type = "ol"
- prefixer = proc { |ignored| "<li>" }
-
- when :LABELED then
- list_type = "dl"
- prefixer = proc do |li|
- "<dt><b>" + escape(li.label) + "</b><dd>"
- end
-
- when :NOTE then
- list_type = "table"
- prefixer = proc do |li|
- %{<tr valign="top"><td>#{li.label.gsub(/ /, '&nbsp;')}</td><td>}
- end
- else
- fail "unknown list type"
- end
-
- @output.print "<#{list_type}>"
- list.contents.each do |item|
- if item.kind_of? RDoc::Markup::Flow::LI
- prefix = prefixer.call(item)
- @output.print prefix
- display_flow_item(item, prefix)
- else
- display_flow_item(item)
- end
- end
- @output.print "</#{list_type}>"
- end
-
- def display_verbatim_flow_item(item, prefix=@indent)
- @output.print("<pre>")
- item.body.split(/\n/).each do |line|
- @output.puts conv_html(line)
- end
- @output.puts("</pre>")
- end
-
- private
-
- ATTR_MAP = {
- BOLD => "b>",
- ITALIC => "i>",
- CODE => "tt>"
- }
-
- def update_attributes(current, wanted)
- str = ""
- # first turn off unwanted ones
- off = current & ~wanted
- for quality in [ BOLD, ITALIC, CODE]
- if (off & quality) > 0
- str << "</" + ATTR_MAP[quality]
- end
- end
-
- # now turn on wanted
- for quality in [ BOLD, ITALIC, CODE]
- unless (wanted & quality).zero?
- str << "<" << ATTR_MAP[quality]
- end
- end
- @output.print str
- end
-
- def tag(code)
- @output.print("<#{code}>")
- @output.print(yield)
- @output.print("</#{code}>")
- end
-
- def escape(str)
- str = str.gsub(/&/n, '&amp;')
- str.gsub!(/\"/n, '&quot;')
- str.gsub!(/>/n, '&gt;')
- str.gsub!(/</n, '&lt;')
- str
- end
-
-end
-
-##
-# This formatter reduces extra lines for a simpler output. It improves way
-# output looks for tools like IRC bots.
-
-class RDoc::RI::SimpleFormatter < RDoc::RI::Formatter
-
- ##
- # No extra blank lines
-
- def blankline
- end
-
- ##
- # Display labels only, no lines
-
- def draw_line(label=nil)
- unless label.nil? then
- bold_print(label)
- @output.puts
- end
- end
-
- ##
- # Place heading level indicators inline with heading.
-
- def display_heading(text, level, indent)
- text = strip_attributes(text)
- case level
- when 1
- @output.puts "= " + text.upcase
- when 2
- @output.puts "-- " + text
- else
- @output.print indent, text, "\n"
- end
- end
+# For RubyGems backwards compatibility
+module RDoc::RI::Formatter # :nodoc:
end
-
-RDoc::RI::Formatter::FORMATTERS['plain'] = RDoc::RI::Formatter
-RDoc::RI::Formatter::FORMATTERS['simple'] = RDoc::RI::SimpleFormatter
-RDoc::RI::Formatter::FORMATTERS['bs'] = RDoc::RI::OverstrikeFormatter
-RDoc::RI::Formatter::FORMATTERS['ansi'] = RDoc::RI::AnsiFormatter
-RDoc::RI::Formatter::FORMATTERS['html'] = RDoc::RI::HtmlFormatter
diff --git a/lib/rdoc/ri/gemdirs.rb b/lib/rdoc/ri/gemdirs.rb
deleted file mode 100644
index 0bca992bca..0000000000
--- a/lib/rdoc/ri/gemdirs.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module RDoc::RI::Paths
- begin
- require 'rubygems' unless defined?(Gem)
-
- # HACK dup'd from Gem.latest_partials and friends
- all_paths = []
-
- all_paths = Gem.path.map do |dir|
- Dir[File.join(dir, 'doc/*/ri')]
- end.flatten
-
- ri_paths = {}
-
- all_paths.each do |dir|
- if %r"/([^/]*)-((?:\d+\.)*\d+)/ri\z" =~ dir
- name, version = $1, $2
- ver = Gem::Version.new(version)
- if !ri_paths[name] or ver > ri_paths[name][0]
- ri_paths[name] = [ver, dir]
- end
- end
- end
-
- GEMDIRS = ri_paths.map { |k,v| v.last }.sort
- rescue LoadError
- GEMDIRS = []
- end
-end
diff --git a/lib/rdoc/ri/paths.rb b/lib/rdoc/ri/paths.rb
index 399fbbab00..178142d35f 100644
--- a/lib/rdoc/ri/paths.rb
+++ b/lib/rdoc/ri/paths.rb
@@ -1,78 +1,118 @@
require 'rdoc/ri'
##
-# Encapsulate all the strangeness to do with finding out where to find RDoc
-# files
-#
-# We basically deal with three directories:
-#
-# 1. The 'system' documentation directory, which holds the documentation
-# distributed with Ruby, and which is managed by the Ruby install process
-# 2. The 'site' directory, which contains site-wide documentation added
-# locally.
-# 3. The 'user' documentation directory, stored under the user's own home
-# directory.
-#
-# There's contention about all this, but for now:
-#
-# system:: $datadir/ri/<ver>/system/...
-# site:: $datadir/ri/<ver>/site/...
-# user:: ~/.rdoc
+# The directories where ri data lives.
module RDoc::RI::Paths
#:stopdoc:
require 'rbconfig'
- DOC_DIR = "doc/rdoc"
+ version = RbConfig::CONFIG['ruby_version']
- VERSION = RbConfig::CONFIG['ruby_version']
+ base = File.join RbConfig::CONFIG['datadir'], "ri", version
+ SYSDIR = File.join base, "system"
+ SITEDIR = File.join base, "site"
+ homedir = File.expand_path('~') ||
+ ENV['HOME'] || ENV['USERPROFILE'] || ENV['HOMEPATH']
- if VERSION > '1.9.1'
- if m = /ruby/.match(RbConfig::CONFIG['RUBY_INSTALL_NAME'])
- m = [m.pre_match, m.post_match]
- else
- m = [""] * 2
+ HOMEDIR = if homedir then
+ File.join homedir, ".rdoc"
+ end
+ #:startdoc:
+
+ @gemdirs = nil
+
+ ##
+ # Iterates over each selected path yielding the directory and type.
+ #
+ # Yielded types:
+ # :system:: Where Ruby's ri data is stored. Yielded when +system+ is
+ # true
+ # :site:: Where ri for installed libraries are stored. Yielded when
+ # +site+ is true. Normally no ri data is stored here.
+ # :home:: ~/.ri. Yielded when +home+ is true.
+ # :gem:: ri data for an installed gem. Yielded when +gems+ is true.
+ # :extra:: ri data directory from the command line. Yielded for each
+ # entry in +extra_dirs+
+
+ def self.each system, site, home, gems, *extra_dirs # :yields: directory, type
+ extra_dirs.each do |dir|
+ yield dir, :extra
end
- ri = "#{m[0]}ri#{m[1]}"
- rdoc = "#{m[0]}rdoc#{m[1]}"
- base = File.join(RbConfig::CONFIG['datadir'], ri, VERSION)
- else
- if m = /ruby/.match(RbConfig::CONFIG['RUBY_BASE_NAME'])
- m = [m.pre_match, m.post_match]
- else
- m = [""] * 2
+
+ yield SYSDIR, :system if system
+ yield SITEDIR, :site if site
+ yield HOMEDIR, :home if home
+
+ gemdirs.each do |dir|
+ yield dir, :gem
+ end if gems
+
+ nil
+ end
+
+ ##
+ # The latest installed gems' ri directories
+
+ def self.gemdirs
+ return @gemdirs if @gemdirs
+
+ require 'rubygems' unless defined?(Gem) and defined?(Gem::Enable) and
+ Gem::Enable
+
+ # HACK dup'd from Gem.latest_partials and friends
+ all_paths = []
+
+ all_paths = Gem.path.map do |dir|
+ Dir[File.join(dir, 'doc', '*', 'ri')]
+ end.flatten
+
+ ri_paths = {}
+
+ all_paths.each do |dir|
+ base = File.basename File.dirname(dir)
+ if base =~ /(.*)-((\d+\.)*\d+)/ then
+ name, version = $1, $2
+ ver = Gem::Version.new version
+ if ri_paths[name].nil? or ver > ri_paths[name][0] then
+ ri_paths[name] = [ver, dir]
+ end
+ end
end
- ri = "#{m[0]}ri#{m[1]}"
- rdoc = "#{m[0]}rdoc#{m[1]}"
- base = File.join(RbConfig::CONFIG['ridir'], VERSION)
+
+ @gemdirs = ri_paths.map { |k,v| v.last }.sort
+ rescue LoadError
+ @gemdirs = []
end
- SYSDIR = File.join(base, "system")
- SITEDIR = File.join(base, "site")
- HOMEDIR = (File.expand_path("~/.#{rdoc}") rescue nil)
- autoload(:GEMDIRS, File.expand_path('../gemdirs.rb', __FILE__))
+ ##
+ # Returns existing directories from the selected documentation directories
+ # as an Array.
+ #
+ # See also ::each
- # Returns the selected documentation directories as an Array, or PATH if no
- # overriding directories were given.
+ def self.path(system, site, home, gems, *extra_dirs)
+ path = raw_path system, site, home, gems, *extra_dirs
- def self.path(use_system, use_site, use_home, use_gems, *extra_dirs)
- path = raw_path(use_system, use_site, use_home, use_gems, *extra_dirs)
- return path.select { |directory| File.directory? directory }
+ path.select { |directory| File.directory? directory }
end
- # Returns the selected documentation directories including nonexistent
- # directories. Used to print out what paths were searched if no ri was
- # found.
+ ##
+ # Returns selected documentation directories including nonexistent
+ # directories.
+ #
+ # See also ::each
- def self.raw_path(use_system, use_site, use_home, use_gems, *extra_dirs)
+ def self.raw_path(system, site, home, gems, *extra_dirs)
path = []
- path << extra_dirs unless extra_dirs.empty?
- path << SYSDIR if use_system
- path << SITEDIR if use_site
- path << HOMEDIR if use_home
- path << GEMDIRS if use_gems
- return path.flatten.compact
+ each(system, site, home, gems, *extra_dirs) do |dir, type|
+ path << dir
+ end
+
+ path.compact
end
+
end
+
diff --git a/lib/rdoc/ri/reader.rb b/lib/rdoc/ri/reader.rb
deleted file mode 100644
index de3c8d9afa..0000000000
--- a/lib/rdoc/ri/reader.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-require 'rdoc/ri'
-require 'rdoc/ri/descriptions'
-require 'rdoc/ri/writer'
-require 'rdoc/markup/to_flow'
-
-class RDoc::RI::Reader
-
- def initialize(ri_cache)
- @cache = ri_cache
- end
-
- def top_level_namespace
- [ @cache.toplevel ]
- end
-
- def lookup_namespace_in(target, namespaces)
- result = []
- for n in namespaces
- result.concat(n.contained_modules_matching(target))
- end
- result
- end
-
- def find_class_by_name(full_name)
- names = full_name.split(/::/)
- ns = @cache.toplevel
- for name in names
- ns = ns.contained_class_named(name)
- return nil if ns.nil?
- end
- get_class(ns)
- end
-
- def find_methods(name, is_class_method, namespaces)
- result = []
- namespaces.each do |ns|
- result.concat ns.methods_matching(name, is_class_method)
- end
- result
- end
-
- ##
- # Return the MethodDescription for a given MethodEntry by deserializing the
- # YAML
-
- def get_method(method_entry)
- path = method_entry.path_name
- File.open(path) { |f| RDoc::RI::Description.deserialize(f) }
- end
-
- ##
- # Return a class description
-
- def get_class(class_entry)
- result = nil
- for path in class_entry.path_names
- path = RDoc::RI::Writer.class_desc_path(path, class_entry)
- desc = File.open(path) {|f| RDoc::RI::Description.deserialize(f) }
- if result
- result.merge_in(desc)
- else
- result = desc
- end
- end
- result
- end
-
- ##
- # Return the names of all classes and modules
-
- def full_class_names
- res = []
- find_classes_in(res, @cache.toplevel)
- end
-
- ##
- # Return a list of all classes, modules, and methods
-
- def all_names
- res = []
- find_names_in(res, @cache.toplevel)
- end
-
- private
-
- def find_classes_in(res, klass)
- classes = klass.classes_and_modules
- for c in classes
- res << c.full_name
- find_classes_in(res, c)
- end
- res
- end
-
- def find_names_in(res, klass)
- classes = klass.classes_and_modules
- for c in classes
- res << c.full_name
- res.concat c.all_method_names
- find_names_in(res, c)
- end
- res
- end
-
-end
-
diff --git a/lib/rdoc/ri/store.rb b/lib/rdoc/ri/store.rb
new file mode 100644
index 0000000000..81ffb7e674
--- /dev/null
+++ b/lib/rdoc/ri/store.rb
@@ -0,0 +1,248 @@
+require 'rdoc/code_objects'
+require 'fileutils'
+
+##
+# A set of ri data.
+#
+# The store manages reading and writing ri data for a project (gem, path,
+# etc.) and maintains a cache of methods, classes and ancestors in the
+# store.
+
+class RDoc::RI::Store
+
+ ##
+ # Path this store reads or writes
+
+ attr_accessor :path
+
+ ##
+ # Type of ri datastore this was loaded from. See RDoc::RI::Driver,
+ # RDoc::RI::Paths.
+
+ attr_accessor :type
+
+ attr_reader :cache
+
+ ##
+ # Creates a new Store of +type+ that will load or save to +path+
+
+ def initialize path, type = nil
+ @type = type
+ @path = path
+
+ @cache = {
+ :class_methods => {},
+ :instance_methods => {},
+ :attributes => {},
+ :modules => [],
+ :ancestors => {},
+ }
+ end
+
+ ##
+ # Ancestors cache accessor. Maps a klass name to an Array of its ancestors
+ # in this store. If Foo in this store inherits from Object, Kernel won't be
+ # listed (it will be included from ruby's ri store).
+
+ def ancestors
+ @cache[:ancestors]
+ end
+
+ ##
+ # Attributes cache accessor. Maps a class to an Array of its attributes.
+
+ def attributes
+ @cache[:attributes]
+ end
+
+ ##
+ # Path to the cache file
+
+ def cache_path
+ File.join @path, 'cache.ri'
+ end
+
+ ##
+ # Path to the ri data for +klass_name+
+
+ def class_file klass_name
+ name = klass_name.split('::').last
+ File.join class_path(klass_name), "cdesc-#{name}.ri"
+ end
+
+ ##
+ # Class methods cache accessor. Maps a class to an Array of its class
+ # methods (not full name).
+
+ def class_methods
+ @cache[:class_methods]
+ end
+
+ ##
+ # Path where data for +klass_name+ will be stored (methods or class data)
+
+ def class_path klass_name
+ File.join @path, *klass_name.split('::')
+ end
+
+ ##
+ # Friendly rendition of #path
+
+ def friendly_path
+ case type
+ when :gem then
+ sep = Regexp.union(*['/', File::ALT_SEPARATOR].compact)
+ @path =~ /#{sep}doc#{sep}(.*?)#{sep}ri$/
+ "gem #{$1}"
+ when :home then '~/.ri'
+ when :site then 'ruby site'
+ when :system then 'ruby core'
+ else @path
+ end
+ end
+
+ def inspect # :nodoc:
+ "#<%s:0x%x %s %p>" % [self.class, object_id, @path, modules.sort]
+ end
+
+ ##
+ # Instance methods cache accessor. Maps a class to an Array of its
+ # instance methods (not full name).
+
+ def instance_methods
+ @cache[:instance_methods]
+ end
+
+ ##
+ # Loads cache file for this store
+
+ def load_cache
+ open cache_path, 'rb' do |io|
+ @cache = Marshal.load io.read
+ end
+ rescue Errno::ENOENT
+ end
+
+ ##
+ # Loads ri data for +klass_name+
+
+ def load_class klass_name
+ open class_file(klass_name), 'rb' do |io|
+ Marshal.load io.read
+ end
+ end
+
+ ##
+ # Loads ri data for +method_name+ in +klass_name+
+
+ def load_method klass_name, method_name
+ open method_file(klass_name, method_name), 'rb' do |io|
+ Marshal.load io.read
+ end
+ end
+
+ ##
+ # Path to the ri data for +method_name+ in +klass_name+
+
+ def method_file klass_name, method_name
+ method_name = method_name.split('::').last
+ method_name =~ /#(.*)/
+ method_type = $1 ? 'i' : 'c'
+ method_name = $1 if $1
+
+ method_name = if ''.respond_to? :ord then
+ method_name.gsub(/\W/) { "%%%02x" % $&[0].ord }
+ else
+ method_name.gsub(/\W/) { "%%%02x" % $&[0] }
+ end
+
+ File.join class_path(klass_name), "#{method_name}-#{method_type}.ri"
+ end
+
+ ##
+ # Modules cache accessor. An Array of all the modules (and classes) in the
+ # store.
+
+ def modules
+ @cache[:modules]
+ end
+
+ ##
+ # Writes the cache file for this store
+
+ def save_cache
+ # HACK mongrel-1.1.5 documents its files twice
+ @cache[:attributes]. each do |_, m| m.uniq!; m.sort! end
+ @cache[:class_methods]. each do |_, m| m.uniq!; m.sort! end
+ @cache[:instance_methods].each do |_, m| m.uniq!; m.sort! end
+
+ open cache_path, 'wb' do |io|
+ Marshal.dump @cache, io
+ end
+ end
+
+ ##
+ # Writes the ri data for +klass+
+
+ def save_class klass
+ FileUtils.mkdir_p class_path(klass.full_name)
+
+ @cache[:modules] << klass.full_name
+
+ path = class_file klass.full_name
+
+ begin
+ disk_klass = nil
+
+ open path, 'rb' do |io|
+ disk_klass = Marshal.load io.read
+ end
+
+ klass.merge disk_klass
+ rescue Errno::ENOENT
+ end
+
+ # BasicObject has no ancestors
+ ancestors = klass.ancestors.compact.map do |ancestor|
+ # HACK for classes we don't know about (class X < RuntimeError)
+ String === ancestor ? ancestor : ancestor.full_name
+ end
+
+ @cache[:ancestors][klass.full_name] ||= []
+ @cache[:ancestors][klass.full_name].push(*ancestors)
+
+ attributes = klass.attributes.map do |attribute|
+ "#{attribute.type} #{attribute.name}"
+ end
+
+ unless attributes.empty? then
+ @cache[:attributes][klass.full_name] ||= []
+ @cache[:attributes][klass.full_name].push(*attributes)
+ end
+
+ open path, 'wb' do |io|
+ Marshal.dump klass, io
+ end
+ end
+
+ ##
+ # Writes the ri data for +method+ on +klass+
+
+ def save_method klass, method
+ FileUtils.mkdir_p class_path(klass.full_name)
+
+ cache = if method.singleton then
+ @cache[:class_methods]
+ else
+ @cache[:instance_methods]
+ end
+ cache[klass.full_name] ||= []
+ cache[klass.full_name] << method.name
+
+ open method_file(klass.full_name, method.full_name), 'wb' do |io|
+ Marshal.dump method, io
+ end
+ end
+
+end
+
diff --git a/lib/rdoc/ri/util.rb b/lib/rdoc/ri/util.rb
deleted file mode 100644
index 51cf881bdd..0000000000
--- a/lib/rdoc/ri/util.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'rdoc/ri'
-
-##
-# Break argument into its constituent class or module names, an
-# optional method type, and a method name
-
-class RDoc::RI::NameDescriptor
-
- attr_reader :class_names
- attr_reader :method_name
-
- ##
- # true and false have the obvious meaning. nil means we don't care
-
- attr_reader :is_class_method
-
- ##
- # +arg+ may be
- #
- # 1. A class or module name (optionally qualified with other class or module
- # names (Kernel, File::Stat etc)
- # 2. A method name
- # 3. A method name qualified by a optionally fully qualified class or module
- # name
- #
- # We're fairly casual about delimiters: folks can say Kernel::puts,
- # Kernel.puts, or Kernel\#puts for example. There's one exception: if you
- # say IO::read, we look for a class method, but if you say IO.read, we look
- # for an instance method
-
- def initialize(arg)
- @class_names = []
- separator = nil
-
- tokens = arg.split(/(\.|::|#)/)
-
- # Skip leading '::', '#' or '.', but remember it might
- # be a method name qualifier
- separator = tokens.shift if tokens[0] =~ /^(\.|::|#)/
-
- # Skip leading '::', but remember we potentially have an inst
-
- # leading stuff must be class names
-
- while tokens[0] =~ /^[A-Z]/
- @class_names << tokens.shift
- unless tokens.empty?
- separator = tokens.shift
- break unless separator == "::"
- end
- end
-
- # Now must have a single token, the method name, or an empty array
- unless tokens.empty?
- @method_name = tokens.shift
- # We may now have a trailing !, ?, or = to roll into
- # the method name
- if !tokens.empty? && tokens[0] =~ /^[!?=]$/
- @method_name << tokens.shift
- end
-
- if @method_name =~ /::|\.|#/ or !tokens.empty?
- raise RDoc::RI::Error.new("Bad argument: #{arg}")
- end
- if separator && separator != '.'
- @is_class_method = separator == "::"
- end
- end
- end
-
- # Return the full class name (with '::' between the components) or "" if
- # there's no class name
-
- def full_class_name
- @class_names.join("::")
- end
-
-end
-
diff --git a/lib/rdoc/ri/writer.rb b/lib/rdoc/ri/writer.rb
deleted file mode 100644
index 92aaa1c2da..0000000000
--- a/lib/rdoc/ri/writer.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'fileutils'
-require 'rdoc/ri'
-
-class RDoc::RI::Writer
-
- def self.class_desc_path(dir, class_desc)
- File.join(dir, "cdesc-" + class_desc.name + ".yaml")
- end
-
- ##
- # Convert a name from internal form (containing punctuation) to an external
- # form (where punctuation is replaced by %xx)
-
- def self.internal_to_external(name)
- if ''.respond_to? :ord then
- name.gsub(/\W/) { "%%%02x" % $&[0].ord }
- else
- name.gsub(/\W/) { "%%%02x" % $&[0] }
- end
- end
-
- ##
- # And the reverse operation
-
- def self.external_to_internal(name)
- name.gsub(/%([0-9a-f]{2,2})/) { $1.to_i(16).chr }
- end
-
- def initialize(base_dir)
- @base_dir = base_dir
- end
-
- def remove_class(class_desc)
- FileUtils.rm_rf(path_to_dir(class_desc.full_name))
- end
-
- def add_class(class_desc)
- dir = path_to_dir(class_desc.full_name)
- FileUtils.mkdir_p(dir)
- class_file_name = self.class.class_desc_path(dir, class_desc)
- File.open(class_file_name, "w") do |f|
- f.write(class_desc.serialize)
- end
- end
-
- def add_method(class_desc, method_desc)
- dir = path_to_dir(class_desc.full_name)
- file_name = self.class.internal_to_external(method_desc.name)
- meth_file_name = File.join(dir, file_name)
- if method_desc.is_singleton
- meth_file_name += "-c.yaml"
- else
- meth_file_name += "-i.yaml"
- end
-
- File.open(meth_file_name, "w") do |f|
- f.write(method_desc.serialize)
- end
- end
-
- private
-
- def path_to_dir(class_name)
- File.join(@base_dir, *class_name.split('::'))
- end
-
-end
-