diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb
index 13ad9366ec..fe95de0a4f 100644
--- a/lib/rdoc/ri/driver.rb
+++ b/lib/rdoc/ri/driver.rb
@@ -125,6 +125,8 @@ def self.process_args(argv)
Class::method | Class#method | Class.method | method
+ $global_variable | PREDEFINED_CONSTANT
+
gem_name: | gem_name:README | gem_name:History
ruby: | ruby:NEWS | ruby:globals
@@ -152,9 +154,12 @@ def self.process_args(argv)
#{opt.program_name} zip
#{opt.program_name} rdoc:README
#{opt.program_name} ruby:comments
+ #{opt.program_name} ARGF
+ #{opt.program_name} '$<'
+ #{opt.program_name} '$LOAD_PATH'
Note that shell quoting or escaping may be required for method names
-containing punctuation:
+containing punctuation or for global variables:
#{opt.program_name} 'Array.[]'
#{opt.program_name} compact\\!
@@ -843,6 +848,116 @@ def display_method(name)
display out
end
+ ##
+ # Pre-defined global constants that can be looked up from globals.rdoc
+
+ PREDEFINED_GLOBAL_CONSTANTS = %w[
+ STDIN STDOUT STDERR ARGV ARGF DATA TOPLEVEL_BINDING
+ ].freeze
+
+ ##
+ # Prefixes for pre-defined global constants
+
+ PREDEFINED_GLOBAL_CONSTANT_PREFIXES = %w[RUBY_].freeze
+
+ ##
+ # Returns true if +name+ is a pre-defined global constant like STDIN, STDOUT,
+ # RUBY_VERSION, etc.
+
+ def predefined_global_constant?(name)
+ PREDEFINED_GLOBAL_CONSTANTS.include?(name) ||
+ PREDEFINED_GLOBAL_CONSTANT_PREFIXES.any? { |prefix| name.start_with?(prefix) }
+ end
+
+ ##
+ # Outputs formatted RI data for the global variable or pre-defined constant
+ # +name+. Looks up the documentation in the globals.rdoc page from the system
+ # store.
+
+ def display_global(name)
+ store = @stores.find { |s| s.type == :system }
+
+ raise NotFoundError, name unless store
+
+ begin
+ page = store.load_page('globals.rdoc')
+ rescue RDoc::Store::MissingFileError
+ raise NotFoundError, name
+ end
+
+ document = page.comment.parse
+ section = extract_global_section(document, name)
+
+ raise NotFoundError, name unless section
+
+ display section
+
+ true
+ end
+
+ ##
+ # Extracts the section for global +name+ from +document+.
+ # Returns an RDoc::Markup::Document containing just that section,
+ # or nil if not found.
+ #
+ # The globals.rdoc document has a hierarchical structure with headings:
+ # = Pre-Defined Global Variables (level 1)
+ # == Streams (level 2)
+ # === $< (ARGF or $stdin) (level 3)
+ # paragraph content...
+ # === $> (Default Output) (level 3)
+ # paragraph content...
+ #
+ # This method finds the heading matching +name+ and collects all content
+ # until the next heading at the same or higher level.
+
+ def extract_global_section(document, name)
+ result = RDoc::Markup::Document.new
+ in_section = false # true once we find the matching heading
+ section_level = nil # heading level of the matched section (e.g., 3 for ===)
+
+ document.parts.each do |part|
+ if RDoc::Markup::Heading === part
+ if heading_matches_global?(part, name)
+ # Found our target heading - start capturing content
+ in_section = true
+ section_level = part.level
+ result << part
+ elsif in_section && part.level <= section_level
+ # Hit next section at same or higher level - stop capturing
+ break
+ elsif in_section
+ # Sub-heading within our section - include it
+ result << part
+ end
+ elsif in_section
+ # Non-heading content (paragraphs, code blocks, etc.) - include it
+ result << part
+ end
+ end
+
+ result.empty? ? nil : result
+ end
+
+ ##
+ # Returns true if +heading+ matches the global +name+.
+ # Handles formats like "$< (ARGF or $stdin)", "$< (ARGF...)", or just "STDOUT".
+
+ def heading_matches_global?(heading, name)
+ text = heading.text
+
+ # Direct match: "STDOUT" or "$<"
+ return true if text == name
+ return true if text.start_with?("#{name} ") || text.start_with?("#{name}\t")
+
+ # Match with wrapper: "$< (description)"
+ tt_wrapped = "#{name}"
+ return true if text.start_with?(tt_wrapped)
+ return true if text.start_with?("#{tt_wrapped} ")
+
+ false
+ end
+
##
# Outputs formatted RI data for the class or method +name+.
#
@@ -850,6 +965,9 @@ def display_method(name)
# be guessed, raises an error if +name+ couldn't be guessed.
def display_name(name)
+ # Handle global variables immediately (classes can't start with $)
+ return display_global(name) if name.start_with?('$')
+
if name =~ /\w:(\w|$)/ then
display_page name
return true
@@ -859,10 +977,23 @@ def display_name(name)
display_method name if name =~ /::|#|\./
+ # If no class was found and it's a predefined constant, try globals lookup
+ # This handles ARGV, STDIN, etc. that look like class names but aren't
+ return display_global(name) if predefined_global_constant?(name)
+
true
rescue NotFoundError
+ # Before giving up, check if it's a predefined global constant
+ if predefined_global_constant?(name)
+ begin
+ return display_global(name)
+ rescue NotFoundError
+ # Fall through to original error handling
+ end
+ end
+
matches = list_methods_matching name if name =~ /::|#|\./
- matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.empty?
+ matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.nil? || matches.empty?
raise if matches.empty?
@@ -983,6 +1114,12 @@ def expand_class(klass)
# #expand_class.
def expand_name(name)
+ # Global variables don't need expansion
+ return name if name.start_with?('$')
+
+ # Predefined global constants don't need expansion
+ return name if predefined_global_constant?(name)
+
klass, selector, method = parse_name name
return [selector, method].join if klass.empty?
diff --git a/test/rdoc/ri/driver_test.rb b/test/rdoc/ri/driver_test.rb
index 2d1a2ce741..72f54b70d6 100644
--- a/test/rdoc/ri/driver_test.rb
+++ b/test/rdoc/ri/driver_test.rb
@@ -951,6 +951,147 @@ def test_display_page_list
assert_match %r%OTHER\.rdoc%, out
end
+ def test_display_global_variable
+ util_store
+
+ # Create a globals page in the store
+ globals = @store1.add_file 'globals.rdoc'
+ globals.parser = RDoc::Parser::Simple
+ globals.comment = RDoc::Comment.from_document(doc(
+ head(1, 'Pre-Defined Global Variables'),
+ head(2, 'Streams'),
+ head(3, '$< (ARGF or $stdin)'),
+ para('Points to stream ARGF if not empty, else to stream $stdin; read-only.'),
+ head(3, '$> (Default Standard Output)'),
+ para('An output stream, initially $stdout.')
+ ))
+ @store1.save_page globals
+ @store1.type = :system
+
+ out, = capture_output do
+ @driver.display_global '$<'
+ end
+
+ assert_match %r%\$< \(ARGF or \$stdin\)%, out
+ assert_match %r%Points to stream ARGF%, out
+ refute_match %r%\$>%, out
+ end
+
+ def test_display_global_constant
+ util_store
+
+ # Create a globals page in the store
+ globals = @store1.add_file 'globals.rdoc'
+ globals.parser = RDoc::Parser::Simple
+ globals.comment = RDoc::Comment.from_document(doc(
+ head(1, 'Pre-Defined Global Constants'),
+ head(2, 'Streams'),
+ head(3, 'STDIN'),
+ para('The standard input stream.'),
+ head(3, 'STDOUT'),
+ para('The standard output stream.')
+ ))
+ @store1.save_page globals
+ @store1.type = :system
+
+ out, = capture_output do
+ @driver.display_global 'STDOUT'
+ end
+
+ assert_match %r%STDOUT%, out
+ assert_match %r%standard output stream%, out
+ refute_match %r%STDIN%, out
+ end
+
+ def test_display_global_not_found
+ util_store
+
+ # Create a globals page in the store (without the requested global)
+ globals = @store1.add_file 'globals.rdoc'
+ globals.parser = RDoc::Parser::Simple
+ globals.comment = RDoc::Comment.from_document(doc(
+ head(1, 'Pre-Defined Global Variables'),
+ head(3, '$<'),
+ para('Some doc')
+ ))
+ @store1.save_page globals
+ @store1.type = :system
+
+ assert_raise RDoc::RI::Driver::NotFoundError do
+ @driver.display_global '$NONEXISTENT'
+ end
+ end
+
+ def test_display_global_no_system_store
+ util_store
+ # Store type is :home by default, not :system
+
+ assert_raise RDoc::RI::Driver::NotFoundError do
+ @driver.display_global '$<'
+ end
+ end
+
+ def test_display_name_global_variable
+ util_store
+
+ # Create a globals page in the store
+ globals = @store1.add_file 'globals.rdoc'
+ globals.parser = RDoc::Parser::Simple
+ globals.comment = RDoc::Comment.from_document(doc(
+ head(1, 'Pre-Defined Global Variables'),
+ head(3, '$< (ARGF or $stdin)'),
+ para('Points to stream ARGF.')
+ ))
+ @store1.save_page globals
+ @store1.type = :system
+
+ out, = capture_output do
+ @driver.display_name '$<'
+ end
+
+ assert_match %r%\$<%, out
+ assert_match %r%ARGF%, out
+ end
+
+ def test_display_name_predefined_constant
+ util_store
+
+ # Create a globals page in the store
+ globals = @store1.add_file 'globals.rdoc'
+ globals.parser = RDoc::Parser::Simple
+ globals.comment = RDoc::Comment.from_document(doc(
+ head(1, 'Pre-Defined Global Constants'),
+ head(3, 'ARGV'),
+ para('An array of the given command-line arguments.')
+ ))
+ @store1.save_page globals
+ @store1.type = :system
+
+ out, = capture_output do
+ @driver.display_name 'ARGV'
+ end
+
+ assert_match %r%ARGV%, out
+ assert_match %r%command-line arguments%, out
+ end
+
+ def test_predefined_global_constant?
+ assert @driver.predefined_global_constant?('STDIN')
+ assert @driver.predefined_global_constant?('STDOUT')
+ assert @driver.predefined_global_constant?('STDERR')
+ assert @driver.predefined_global_constant?('ARGV')
+ assert @driver.predefined_global_constant?('ARGF')
+ assert @driver.predefined_global_constant?('DATA')
+ assert @driver.predefined_global_constant?('TOPLEVEL_BINDING')
+ assert @driver.predefined_global_constant?('RUBY_VERSION')
+ assert @driver.predefined_global_constant?('RUBY_PLATFORM')
+
+ refute @driver.predefined_global_constant?('ENV') # ENV is a class, not a simple constant
+ refute @driver.predefined_global_constant?('MyClass')
+ refute @driver.predefined_global_constant?('Foo')
+ refute @driver.predefined_global_constant?('$<')
+ end
+
def test_expand_class
util_store