diff options
author | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-16 18:49:26 +0900 |
---|---|---|
committer | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-30 00:39:06 +0900 |
commit | fcbf63e62c627deae76c1b8cb8c0876c536ed811 (patch) | |
tree | 64cb17de3f41a2b6fef2368028fbd00349946994 /jni/ruby/lib/rdoc/ri/driver.rb |
Fresh start
Diffstat (limited to 'jni/ruby/lib/rdoc/ri/driver.rb')
-rw-r--r-- | jni/ruby/lib/rdoc/ri/driver.rb | 1497 |
1 files changed, 1497 insertions, 0 deletions
diff --git a/jni/ruby/lib/rdoc/ri/driver.rb b/jni/ruby/lib/rdoc/ri/driver.rb new file mode 100644 index 0000000..39064c1 --- /dev/null +++ b/jni/ruby/lib/rdoc/ri/driver.rb @@ -0,0 +1,1497 @@ +require 'abbrev' +require 'optparse' + +begin + require 'readline' +rescue LoadError +end + +begin + require 'win32console' +rescue LoadError +end + +require 'rdoc' + +## +# For RubyGems backwards compatibility + +require 'rdoc/ri/formatter' + +## +# 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 + +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 + + ## + # Name that wasn't found + + alias name message + + def message # :nodoc: + "Nothing known about #{super}" + end + end + + ## + # Show all method documentation following a class or module + + attr_accessor :show_all + + ## + # An RDoc::RI::Store for each entry in the RI path + + 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[:interactive] = false + options[:profile] = false + options[:show_all] = false + options[:use_cache] = true + options[:use_stdout] = !$stdout.tty? + options[:width] = 72 + + # By default all standard paths are used. + options[:use_system] = true + options[:use_site] = true + options[:use_home] = true + options[:use_gems] = true + options[:extra_doc_dirs] = [] + + return options + end + + ## + # 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 + + opt.banner = <<-EOT +Usage: #{opt.program_name} [options] [names...] + +Where name can be: + + Class | Module | Module::Class + + Class::method | Class#method | Class.method | method + + gem_name: | gem_name:README | gem_name:History + +All class names may be abbreviated to their minimum unambiguous form. If a name +is ambiguous, all valid options will be listed. + +A '.' matches either class or instance methods, while #method +matches only instance and ::method matches only class methods. + +README and other files may be displayed by prefixing them with the gem name +they're contained in. If the gem name is followed by a ':' all files in the +gem will be shown. The file name extension may be omitted where it is +unambiguous. + +For example: + + #{opt.program_name} Fil + #{opt.program_name} File + #{opt.program_name} File.new + #{opt.program_name} zip + #{opt.program_name} rdoc:README + +Note that shell quoting or escaping may be required for method names containing +punctuation: + + #{opt.program_name} 'Array.[]' + #{opt.program_name} compact\\! + +To see the default directories ri will search, run: + + #{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. + +ri options may be set in the 'RI' environment variable. + +The ri pager can be set with the 'RI_PAGER' environment variable or the +'PAGER' environment variable. + EOT + + opt.separator nil + opt.separator "Options:" + + opt.separator nil + + opt.on("--[no-]interactive", "-i", + "In interactive mode you can repeatedly", + "look up methods with autocomplete.") do |interactive| + options[:interactive] = interactive + end + + opt.separator nil + + opt.on("--[no-]all", "-a", + "Show all documentation for a class or", + "module.") do |show_all| + options[:show_all] = show_all + end + + opt.separator nil + + opt.on("--[no-]list", "-l", + "List classes ri knows about.") do |list| + options[:list] = list + end + + opt.separator nil + + opt.on("--[no-]pager", + "Send output directly to stdout,", + "rather than to a pager.") do |use_pager| + options[:use_stdout] = !use_pager + end + + opt.separator nil + + opt.on("-T", + "Synonym for --no-pager") do + options[:use_stdout] = true + end + + opt.separator nil + + opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger, + "Set the width of the output.") do |width| + options[:width] = width + end + + opt.separator nil + + opt.on("--server [PORT]", Integer, + "Run RDoc server on the given port.", + "The default port is 8214.") do |port| + options[:server] = port || 8214 + end + + opt.separator nil + + formatters = RDoc::Markup.constants.grep(/^To[A-Z][a-z]+$/).sort + formatters = formatters.sort.map do |formatter| + formatter.to_s.sub('To', '').downcase + end + formatters -= %w[html label test] # remove useless output formats + + 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.separator "Data source options:" + opt.separator nil + + opt.on("--[no-]list-doc-dirs", + "List the directories from which ri will", + "source documentation on stdout and exit.") do |list_doc_dirs| + options[:list_doc_dirs] = list_doc_dirs + end + + opt.separator nil + + opt.on("--doc-dir=DIRNAME", "-d", Array, + "List of directories from which to source", + "documentation in addition to the standard", + "directories. May be repeated.") do |value| + value.each do |dir| + unless File.directory? dir then + raise OptionParser::InvalidArgument, "#{dir} is not a directory" + end + + options[:extra_doc_dirs] << File.expand_path(dir) + end + end + + opt.separator nil + + opt.on("--no-standard-docs", + "Do not include documentation from", + "the Ruby standard library, site_lib,", + "installed gems, or ~/.rdoc.", + "Use with --doc-dir") do + options[:use_system] = false + options[:use_site] = false + options[:use_gems] = false + options[:use_home] = false + end + + opt.separator nil + + opt.on("--[no-]system", + "Include documentation from Ruby's standard", + "library. Defaults to true.") do |value| + options[:use_system] = value + end + + opt.separator nil + + opt.on("--[no-]site", + "Include documentation from libraries", + "installed in site_lib.", + "Defaults to true.") do |value| + options[:use_site] = value + end + + opt.separator nil + + opt.on("--[no-]gems", + "Include documentation from RubyGems.", + "Defaults to true.") do |value| + options[:use_gems] = value + end + + opt.separator nil + + opt.on("--[no-]home", + "Include documentation stored in ~/.rdoc.", + "Defaults to true.") do |value| + options[:use_home] = value + end + + opt.separator nil + opt.separator "Debug options:" + opt.separator nil + + opt.on("--[no-]profile", + "Run with the ruby profiler") do |value| + options[:profile] = value + end + + opt.separator nil + + opt.on("--dump=CACHE", File, + "Dumps data from an ri cache or data file") do |value| + options[:dump_path] = value + end + end + + argv = ENV['RI'].to_s.split.concat argv + + opts.parse! argv + + options[:names] = argv + + options[:use_stdout] ||= !$stdout.tty? + options[:use_stdout] ||= options[:interactive] + options[:width] ||= 72 + + options + + rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e + puts opts + puts + puts e + exit 1 + end + + ## + # 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 + + ## + # 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] + @list = options[:list] + + @doc_dirs = [] + @stores = [] + + RDoc::RI::Paths.each(options[:use_system], options[:use_site], + options[:use_home], options[:use_gems], + *options[:extra_doc_dirs]) do |path, type| + @doc_dirs << path + + store = RDoc::RI::Store.new path, type + store.load_cache + @stores << store + end + + @list_doc_dirs = options[:list_doc_dirs] + + @interactive = options[:interactive] + @server = options[:server] + @use_stdout = options[:use_stdout] + @show_all = options[:show_all] + + # pager process for jruby + @jruby_pager_process = nil + end + + ## + # Adds paths for undocumented classes +also_in+ to +out+ + + 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 + + ## + # 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' + + superclass = superclass.full_name unless String === superclass + + "#{name} < #{superclass}" + end + + out << RDoc::Markup::Heading.new(1, heading) + out << RDoc::Markup::BlankLine.new + end + + ## + # Adds "(from ...)" to +out+ for +store+ + + def add_from out, store + out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") + end + + ## + # Adds +extends+ to +out+ + + def add_extends out, extends + add_extension_modules out, 'Extended by', extends + end + + ## + # Adds a list of +extensions+ to this module of the given +type+ to +out+. + # add_includes and add_extends call this, so you should use those directly. + + def add_extension_modules out, type, extensions + return if extensions.empty? + + out << RDoc::Markup::Rule.new(1) + out << RDoc::Markup::Heading.new(1, "#{type}:") + + extensions.each do |modules, store| + if modules.length == 1 then + add_extension_modules_single out, store, modules.first + else + add_extension_modules_multiple out, store, modules + end + end + end + + ## + # Renders multiple included +modules+ from +store+ to +out+. + + def add_extension_modules_multiple out, store, modules # :nodoc: + out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") + + wout, with = modules.partition { |incl| incl.comment.empty? } + + out << RDoc::Markup::BlankLine.new unless with.empty? + + with.each do |incl| + out << RDoc::Markup::Paragraph.new(incl.name) + out << RDoc::Markup::BlankLine.new + out << incl.comment + end + + unless wout.empty? then + verb = RDoc::Markup::Verbatim.new + + wout.each do |incl| + verb.push incl.name, "\n" + end + + out << verb + end + end + + ## + # Adds a single extension module +include+ from +store+ to +out+ + + def add_extension_modules_single out, store, include # :nodoc: + name = include.name + path = store.friendly_path + out << RDoc::Markup::Paragraph.new("#{name} (from #{path})") + + if include.comment then + out << RDoc::Markup::BlankLine.new + out << include.comment + end + end + + ## + # Adds +includes+ to +out+ + + def add_includes out, includes + add_extension_modules out, 'Includes', includes + end + + ## + # Looks up the method +name+ and adds it to +out+ + + def add_method out, name + filtered = lookup_method name + + method_out = method_document name, filtered + + out.concat method_out.parts + end + + ## + # Adds documentation for all methods in +klass+ to +out+ + + def add_method_documentation out, klass + klass.method_list.each do |method| + begin + add_method out, method.full_name + rescue NotFoundError + next + end + end + end + + ## + # Adds a list of +methods+ to +out+ with a heading of +name+ + + def add_method_list out, methods, name + return if methods.empty? + + out << RDoc::Markup::Heading.new(1, "#{name}:") + out << RDoc::Markup::BlankLine.new + + if @use_stdout and !@interactive then + out.concat methods.map { |method| + RDoc::Markup::Verbatim.new method + } + else + out << RDoc::Markup::IndentedParagraph.new(2, methods.join(', ')) + end + + out << RDoc::Markup::BlankLine.new + end + + ## + # 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.concat klasses + unexamined.concat klasses + end + + ancestors.reverse + end + + ## + # For RubyGems backwards compatibility + + def class_cache # :nodoc: + end + + ## + # Builds a RDoc::Markup::Document from +found+, +klasess+ and +includes+ + + def class_document name, found, klasses, includes, extends + also_in = [] + + out = RDoc::Markup::Document.new + + add_class out, name, klasses + + add_includes out, includes + add_extends out, extends + + found.each do |store, klass| + render_class out, store, klass, also_in + end + + add_also_in out, also_in + + out + end + + ## + # Adds the class +comment+ to +out+. + + def class_document_comment out, comment # :nodoc: + unless comment.empty? then + out << RDoc::Markup::Rule.new(1) + + if comment.merged? then + parts = comment.parts + parts = parts.zip [RDoc::Markup::BlankLine.new] * parts.length + parts.flatten! + parts.pop + + out.concat parts + else + out << comment + end + end + end + + ## + # Adds the constants from +klass+ to the Document +out+. + + def class_document_constants out, klass # :nodoc: + return if klass.constants.empty? + + 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.items.concat constants.map { |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) + } + + out << list + out << RDoc::Markup::BlankLine.new + end + + ## + # 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 + + ## + # Returns the stores wherein +name+ is found along with the classes, + # extends and includes that match it + + def classes_and_includes_and_extends_for name + klasses = [] + extends = [] + includes = [] + + found = @stores.map do |store| + begin + klass = store.load_class name + klasses << klass + extends << [klass.extends, store] if klass.extends + includes << [klass.includes, store] if klass.includes + [store, klass] + rescue RDoc::Store::MissingFileError + end + end.compact + + extends.reject! do |modules,| modules.empty? end + includes.reject! do |modules,| modules.empty? end + + [found, klasses, includes, extends] + end + + ## + # Completes +name+ based on the caches. For Readline + + def complete name + completions = [] + + klass, selector, method = parse_name name + + complete_klass name, klass, selector, method, completions + complete_method name, klass, selector, completions + + completions.sort.uniq + end + + def complete_klass name, klass, selector, method, completions # :nodoc: + klasses = classes.keys + + # may need to include Foo when given Foo:: + klass_name = method ? name : klass + + if name !~ /#|\./ then + completions.replace klasses.grep(/^#{Regexp.escape klass_name}[^:]*$/) + completions.concat klasses.grep(/^#{Regexp.escape name}[^:]*$/) if + name =~ /::$/ + + completions << klass if classes.key? klass # to complete a method name + elsif selector then + completions << klass if classes.key? klass + elsif classes.key? klass_name then + completions << klass_name + end + end + + def complete_method name, klass, selector, completions # :nodoc: + 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.concat methods + end + end + + ## + # Converts +document+ to text and writes it to the pager + + def display document + page do |io| + text = document.accept formatter(io) + + io.write text + end + end + + ## + # Outputs formatted RI data for class +name+. Groups undocumented classes + + def display_class name + return if name =~ /#|\./ + + found, klasses, includes, extends = + classes_and_includes_and_extends_for name + + return if found.empty? + + out = class_document name, found, klasses, includes, extends + + display out + end + + ## + # Outputs formatted RI data for method +name+ + + def display_method name + out = RDoc::Markup::Document.new + + add_method out, name + + 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 + if name =~ /\w:(\w|$)/ then + display_page name + return true + end + + 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(/^#{Regexp.escape name}/) if matches.empty? + + raise if matches.empty? + + page do |io| + io.puts "#{name} not found, maybe you meant:" + io.puts + io.puts matches.sort.join("\n") + end + + false + end + + ## + # Displays each name in +name+ + + def display_names names + names.each do |name| + name = expand_name name + + display_name name + end + end + + ## + # Outputs formatted RI data for page +name+. + + def display_page name + store_name, page_name = name.split ':', 2 + + store = @stores.find { |s| s.source == store_name } + + return display_page_list store if page_name.empty? + + pages = store.cache[:pages] + + unless pages.include? page_name then + found_names = pages.select do |n| + n =~ /#{Regexp.escape page_name}\.[^.]+$/ + end + + if found_names.length.zero? then + return display_page_list store, pages + elsif found_names.length > 1 then + return display_page_list store, found_names, page_name + end + + page_name = found_names.first + end + + page = store.load_page page_name + + display page.comment + end + + ## + # Outputs a formatted RI page list for the pages in +store+. + + def display_page_list store, pages = store.cache[:pages], search = nil + out = RDoc::Markup::Document.new + + title = if search then + "#{search} pages" + else + 'Pages' + end + + out << RDoc::Markup::Heading.new(1, "#{title} in #{store.friendly_path}") + out << RDoc::Markup::BlankLine.new + + list = RDoc::Markup::List.new(:BULLET) + + pages.each do |page| + list << RDoc::Markup::Paragraph.new(page) + end + + out << list + + display out + 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 + + subset = classes.keys.select do |klass_name| + klass_name =~ /^#{expanded}[^:]*$/ + end + + abbrevs = Abbrev.abbrev subset + + expanded = abbrevs[short] + + raise NotFoundError, short unless expanded + + expanded.dup + end + end + + ## + # 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? + + case selector + when ':' then + [find_store(klass), selector, method] + else + [expand_class(klass), selector, method] + end.join + end + + ## + # Filters the methods in +found+ trying to find a match for +name+. + + def filter_methods found, name + regexp = name_regexp name + + filtered = found.find_all do |store, methods| + methods.any? { |method| method.full_name =~ regexp } + end + + return filtered unless filtered.empty? + + found + end + + ## + # 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 + + methods.each do |item| + yield(*item) # :yields: store, klass, ancestor, types, method + end + + self + end + + ## + # Finds the given +pager+ for jruby. Returns an IO if +pager+ was found. + # + # Returns false if +pager+ does not exist. + # + # Returns nil if the jruby JVM doesn't support ProcessBuilder redirection + # (1.6 and older). + + def find_pager_jruby pager + require 'java' + require 'shellwords' + + return nil unless java.lang.ProcessBuilder.constants.include? :Redirect + + pager = Shellwords.split pager + + pb = java.lang.ProcessBuilder.new(*pager) + pb = pb.redirect_output java.lang.ProcessBuilder::Redirect::INHERIT + + @jruby_pager_process = pb.start + + input = @jruby_pager_process.output_stream + + io = input.to_io + io.sync = true + io + rescue java.io.IOException + false + end + + ## + # Finds a store that matches +name+ which can be the name of a gem, "ruby", + # "home" or "site". + # + # See also RDoc::Store#source + + def find_store name + @stores.each do |store| + source = store.source + + return source if source == name + + return source if + store.type == :gem and source =~ /^#{Regexp.escape name}-\d/ + end + + raise RDoc::RI::Driver::NotFoundError, name + end + + ## + # 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(io) + if @formatter_klass then + @formatter_klass.new + elsif paging? or !io.tty? then + RDoc::Markup::ToBs.new + else + RDoc::Markup::ToAnsi.new + end + end + + ## + # Runs ri interactively using Readline if it is available. + + def interactive + puts "\nEnter the method name you want to look up." + + if defined? Readline then + Readline.completion_proc = method :complete + puts "You can use tab to autocomplete." + end + + puts "Enter a blank line to exit.\n\n" + + loop do + name = if defined? Readline then + Readline.readline ">> " + else + print ">> " + $stdin.gets + end + + return if name.nil? or name.empty? + + name = expand_name name.strip + + begin + display_name name + rescue NotFoundError => e + puts e.message + end + end + + rescue Interrupt + exit + end + + ## + # Is +file+ in ENV['PATH']? + + def in_path? file + return true if file =~ %r%\A/% and File.exist? file + + ENV['PATH'].split(File::PATH_SEPARATOR).any? do |path| + File.exist? File.join(path, file) + end + end + + ## + # Lists classes known to ri starting with +names+. If +names+ is empty all + # known classes are shown. + + def list_known_classes names = [] + classes = [] + + stores.each do |store| + classes << store.module_names + end + + classes = classes.flatten.uniq.sort + + unless names.empty? then + filter = Regexp.union names.map { |name| /^#{name}/ } + + classes = classes.grep filter + end + + page do |io| + if paging? or io.tty? then + if names.empty? then + io.puts "Classes and Modules known to ri:" + else + io.puts "Classes and Modules starting with #{names.join ', '}:" + end + io.puts + end + + io.puts classes.join("\n") + end + 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] + + if methods then + matches = methods.grep(/^#{Regexp.escape method.to_s}/) + + matches = matches.map do |match| + "#{klass}##{match}" + end + + found.concat matches + end + end + + if types == :class or types == :both then + methods = store.class_methods[ancestor] + + next unless methods + matches = methods.grep(/^#{Regexp.escape method.to_s}/) + + matches = matches.map do |match| + "#{klass}::#{match}" + end + + found.concat matches + end + end + + found.uniq + end + + ## + # 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}" + rescue RDoc::Store::MissingFileError => e + comment = RDoc::Comment.new("missing documentation at #{e.file}").parse + + method = RDoc::AnyMethod.new nil, name + method.comment = comment + 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 + [:class, :both].include? types + + methods << load_method(store, :instance_methods, ancestor, '#', method) if + [:instance, :both].include? types + + found << [store, methods.compact] + end + + found.reject do |path, methods| methods.empty? end + end + + ## + # Returns a filtered list of methods matching +name+ + + def lookup_method name + found = load_methods_matching name + + raise NotFoundError, name if found.empty? + + filter_methods found, name + end + + ## + # Builds a RDoc::Markup::Document from +found+, +klasses+ and +includes+ + + def method_document name, filtered + out = RDoc::Markup::Document.new + + out << RDoc::Markup::Heading.new(1, name) + out << RDoc::Markup::BlankLine.new + + filtered.each do |store, methods| + methods.each do |method| + render_method out, store, method, name + end + end + + out + 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 + + ## + # Returns a regular expression for +name+ that will match an + # RDoc::AnyMethod's name. + + def name_regexp name + klass, type, name = parse_name name + + case type + when '#', '::' then + /^#{klass}#{type}#{Regexp.escape name}$/ + else + /^#{klass}(#|::)#{Regexp.escape name}$/ + end + end + + ## + # Paginates output through a pager program. + + def page + if pager = setup_pager then + begin + yield pager + ensure + pager.close + @jruby_pager_process.wait_for if @jruby_pager_process + end + else + yield $stdout + end + rescue Errno::EPIPE + ensure + @paging = false + end + + ## + # Are we using a pager? + + def paging? + @paging + end + + ## + # Extracts 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[1] == ':' then + klass = parts.shift + type = parts.shift + meth = parts.join + elsif parts[-2] != '::' or parts.last !~ /^[A-Z]/ then + meth = parts.pop + type = parts.pop + end + + klass ||= parts.join + + [klass, type, meth] + end + + ## + # Renders the +klass+ from +store+ to +out+. If the klass has no + # documentable items the class is added to +also_in+ instead. + + def render_class out, store, klass, also_in # :nodoc: + comment = klass.comment + # TODO the store's cache should always return an empty Array + 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.empty? and class_methods.empty? then + also_in << store + return + end + + add_from out, store + + class_document_comment out, comment + + if class_methods or instance_methods or not klass.constants.empty? then + out << RDoc::Markup::Rule.new(1) + end + + class_document_constants out, klass + + add_method_list out, class_methods, 'Class methods' + add_method_list out, instance_methods, 'Instance methods' + add_method_list out, attributes, 'Attributes' + + add_method_documentation out, klass if @show_all + end + + def render_method out, store, method, name # :nodoc: + 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) + + render_method_arguments out, method.arglists + render_method_superclass out, method + render_method_comment out, method + end + + def render_method_arguments out, arglists # :nodoc: + return unless arglists + + arglists = arglists.chomp.split "\n" + arglists = arglists.map { |line| line + "\n" } + out << RDoc::Markup::Verbatim.new(*arglists) + out << RDoc::Markup::Rule.new(1) + end + + def render_method_comment out, method # :nodoc: + out << RDoc::Markup::BlankLine.new + out << method.comment + out << RDoc::Markup::BlankLine.new + end + + def render_method_superclass out, method # :nodoc: + return unless + method.respond_to?(:superclass_method) and method.superclass_method + + out << RDoc::Markup::BlankLine.new + out << RDoc::Markup::Heading.new(4, "(Uses superclass method #{method.superclass_method})") + out << RDoc::Markup::Rule.new(1) + end + + ## + # Looks up and displays ri data according to the options given. + + def run + if @list_doc_dirs then + puts @doc_dirs + elsif @list then + list_known_classes @names + elsif @server then + start_server + elsif @interactive or @names.empty? then + interactive + 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 + + jruby = Object.const_defined?(:RUBY_ENGINE) && RUBY_ENGINE == 'jruby' + + pagers = [ENV['RI_PAGER'], ENV['PAGER'], 'pager', 'less', 'more'] + + pagers.compact.uniq.each do |pager| + next unless pager + + pager_cmd = pager.split.first + + next unless in_path? pager_cmd + + if jruby then + case io = find_pager_jruby(pager) + when nil then break + when false then next + else io + end + else + io = IO.popen(pager, 'w') rescue next + end + + next if $? and $?.pid == io.pid and $?.exited? # pager didn't work + + @paging = true + + return io + end + + @use_stdout = true + + nil + end + + ## + # Starts a WEBrick server for ri. + + def start_server + require 'webrick' + + server = WEBrick::HTTPServer.new :Port => @server + + extra_doc_dirs = @stores.map {|s| s.type == :extra ? s.path : nil}.compact + + server.mount '/', RDoc::Servlet, nil, extra_doc_dirs + + trap 'INT' do server.shutdown end + trap 'TERM' do server.shutdown end + + server.start + end + +end + |