diff --git a/doc/EXTEND_IRB.md b/doc/EXTEND_IRB.md
index 684b0b7b1..09f811edc 100644
--- a/doc/EXTEND_IRB.md
+++ b/doc/EXTEND_IRB.md
@@ -120,3 +120,33 @@ Helper methods
conf Returns the current context.
my_helper This is a test helper
```
+
+## Documentation providers
+
+The `show_doc` command and the documentation dialog of the autocompletion (shown next to the completion candidates, and expanded with `Alt+d`) look up RI data by default. `IRB.doc_providers` is the ordered list of the backends they consult, so a library can serve documentation from another source, such as a project-local documentation store or a manual in another language. Providers earlier in the list take precedence; the built-in `IRB::RDocDocumentProvider` is the last resort by default.
+
+A provider is any object that implements `document`; `dialog_contents` is optional.
+
+### Example
+
+```rb
+class MyDocProvider
+ # name is written the way RI accepts it: "Array", "Array#each", "Array.new",
+ # or "String.gsub" (the completion uses a dot even for instance methods).
+ # Return the documentation as a String (ANSI escape sequences are allowed;
+ # it is shown through IRB's pager), or nil to let the next provider answer.
+ def document(name)
+ MyManual.lookup(name)&.to_ansi
+ end
+
+ # Optional. Return the preview shown in the documentation dialog as an
+ # Array of lines that fit in `width` columns, or nil.
+ def dialog_contents(name, width)
+ MyManual.lookup(name)&.summary_lines(width)
+ end
+end
+
+IRB.doc_providers.unshift(MyDocProvider.new)
+```
+
+`show_doc` without an argument always starts RI's interactive session; it does not consult the providers.
diff --git a/lib/irb/command/show_doc.rb b/lib/irb/command/show_doc.rb
index 8a2188e4e..b9d12137c 100644
--- a/lib/irb/command/show_doc.rb
+++ b/lib/irb/command/show_doc.rb
@@ -25,26 +25,39 @@ class ShowDoc < Base
def execute(arg)
# Accept string literal for backward compatibility
name = unwrap_string_literal(arg)
- require 'rdoc/ri/driver'
- unless ShowDoc.const_defined?(:Ri)
- opts = RDoc::RI::Driver.process_args([])
- ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts))
+ if name.nil?
+ if rdoc_provider.available?
+ rdoc_provider.interactive
+ else
+ warn RDocDocumentProvider::NOT_INSTALLED_MESSAGE
+ end
+ return
end
- if name.nil?
- Ri.interactive
- else
- begin
- Ri.display_name(name)
- rescue RDoc::RI::Error
- puts $!.message
+ IRB.doc_providers.each do |provider|
+ document = provider.document(name)
+ if document
+ Pager.page_content(document)
+ return
end
end
+ if rdoc_provider.available?
+ puts rdoc_provider.not_found_message(name)
+ else
+ warn RDocDocumentProvider::NOT_INSTALLED_MESSAGE
+ end
+ nil
+ rescue SystemExit
+ # RI's interactive session exits on Ctrl-C
nil
- rescue LoadError, SystemExit
- warn "Can't display document because `rdoc` is not installed."
+ end
+
+ private
+
+ def rdoc_provider
+ @rdoc_provider ||= IRB.doc_providers.find { |provider| provider.is_a?(RDocDocumentProvider) } || RDocDocumentProvider.new
end
end
end
diff --git a/lib/irb/doc_provider.rb b/lib/irb/doc_provider.rb
new file mode 100644
index 000000000..a22756d97
--- /dev/null
+++ b/lib/irb/doc_provider.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require_relative 'color'
+
+module IRB
+ # Returns the documentation providers consulted, in order, by the +show_doc+
+ # command and by the documentation dialog of the autocompletion (Alt+d).
+ #
+ # By default the list only contains an RDocDocumentProvider, which looks up
+ # RI data. Other backends can be added, for example from +.irbrc+; providers
+ # earlier in the list take precedence:
+ #
+ # IRB.doc_providers.unshift(MyDocProvider.new)
+ #
+ # A provider is any object that implements the following methods:
+ #
+ # +document(name)+::
+ # Returns the documentation of +name+ as a String, or +nil+ when the
+ # provider has nothing for +name+ so that the next provider is consulted.
+ # +name+ is written the way RI accepts it: "Array",
+ # "Array#each", "Array.new", or "String.gsub"
+ # (the completion uses a dot even for instance methods). The returned
+ # String is shown through IRB::Pager and may contain ANSI escape sequences.
+ #
+ # +dialog_contents(name, width)+::
+ # Optional. Returns the preview shown in the documentation dialog as an
+ # Array of lines that fit in +width+ columns, or +nil+. The dialog skips
+ # providers that do not implement this method.
+ class << self
+ def doc_providers
+ @doc_providers ||= [RDocDocumentProvider.new]
+ end
+ end
+
+ # The default documentation provider. It looks up RI data (RDoc::RI::Driver),
+ # including the directories in IRB.conf[:EXTRA_DOC_DIRS].
+ class RDocDocumentProvider
+ NOT_INSTALLED_MESSAGE = "Can't display document because `rdoc` is not installed."
+
+ # Returns true when RDoc can be loaded.
+ def available?
+ require 'rdoc'
+ true
+ rescue LoadError
+ false
+ end
+
+ def document(name)
+ document = retrieve_document(name)
+ return unless document
+
+ formatter = Color.colorable? ? RDoc::Markup::ToAnsi.new : RDoc::Markup::ToBs.new
+ document.accept(formatter)
+ end
+
+ def dialog_contents(name, width)
+ document = retrieve_document(name)
+ return unless document
+
+ formatter = RDoc::Markup::ToAnsi.new
+ formatter.width = width
+ document.accept(formatter).split("\n")
+ end
+
+ # Starts the interactive session of RI, as +show_doc+ without an argument does.
+ def interactive
+ driver.interactive
+ end
+
+ # Returns the message +show_doc+ prints when no provider knows +name+.
+ # It reports the closest names RI knows, like the +ri+ command does.
+ def not_found_message(name)
+ matches = name.match?(/::|#|\./) ? driver.list_methods_matching(name) : []
+ matches = driver.classes.keys.grep(/\A#{Regexp.escape(name)}/) if matches.empty?
+ return "Nothing known about #{name}" if matches.empty?
+
+ "#{name} not found, maybe you meant:\n\n#{matches.sort.join("\n")}"
+ end
+
+ private
+
+ def driver
+ return @driver if defined?(@driver)
+
+ require 'rdoc'
+ require 'rdoc/ri/driver'
+ options = {}
+ extra_doc_dirs = IRB.conf[:EXTRA_DOC_DIRS]
+ options[:extra_doc_dirs] = extra_doc_dirs unless extra_doc_dirs.nil? || extra_doc_dirs.empty?
+ @driver = RDoc::RI::Driver.new(options)
+ end
+
+ # Returns an RDoc::Markup::Document for +name+, or nil when RI does not know it.
+ def retrieve_document(name)
+ return unless available?
+
+ return retrieve_page(name) if name.match?(/\w:(\w|$)/)
+
+ name = driver.expand_name(name)
+
+ if name.match?(/#|\./)
+ document = RDoc::Markup::Document.new
+ driver.add_method(document, name)
+ else
+ found, klasses, includes, extends = driver.classes_and_includes_and_extends_for(name)
+ if found.empty?
+ document = RDoc::Markup::Document.new
+ driver.add_method(document, name)
+ else
+ return driver.class_document(name, found, klasses, includes, extends)
+ end
+ end
+ driver.expand_rdoc_refs_at_the_bottom(document) if driver.respond_to?(:expand_rdoc_refs_at_the_bottom)
+ document
+ rescue RDoc::RI::Driver::NotFoundError
+ nil
+ end
+
+ # Returns the document of an RI page such as "ruby:syntax".
+ def retrieve_page(name)
+ store_name, page_name = name.split(':', 2)
+ store = driver.stores.find { |s| s.source == store_name }
+ return unless store
+
+ pages = store.cache[:pages]
+ unless pages.include?(page_name)
+ candidates = pages.grep(/#{Regexp.escape(page_name)}\.[^.]+$/)
+ return unless candidates.size == 1
+
+ page_name = candidates.first
+ end
+ store.load_page(page_name).comment.parse
+ end
+ end
+end
diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb
index 10758cc83..52f6a10f1 100644
--- a/lib/irb/input-method.rb
+++ b/lib/irb/input-method.rb
@@ -5,6 +5,7 @@
#
require_relative 'completion'
+require_relative 'doc_provider'
require_relative "history"
require 'io/console'
require 'reline'
@@ -289,12 +290,8 @@ def initialize(completor)
Reline.dig_perfect_match_proc = ->(matched) { display_document(matched) }
Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE]
- if IRB.conf[:USE_AUTOCOMPLETE]
- begin
- require 'rdoc'
- Reline.add_dialog_proc(:show_doc, show_doc_dialog_proc, Reline::DEFAULT_DIALOG_CONTEXT)
- rescue LoadError
- end
+ if IRB.conf[:USE_AUTOCOMPLETE] && doc_dialog_available?
+ Reline.add_dialog_proc(:show_doc, show_doc_dialog_proc, Reline::DEFAULT_DIALOG_CONTEXT)
end
end
@@ -328,17 +325,12 @@ def retrieve_document_target(matched)
end
end
- def rdoc_ri_driver
- return @rdoc_ri_driver if defined?(@rdoc_ri_driver)
+ # Whether some provider in IRB.doc_providers can serve the documentation dialog.
+ def doc_dialog_available?
+ IRB.doc_providers.any? do |provider|
+ next false unless provider.respond_to?(:dialog_contents)
- begin
- require 'rdoc'
- rescue LoadError
- @rdoc_ri_driver = nil
- else
- options = {}
- options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty?
- @rdoc_ri_driver = RDoc::RI::Driver.new(options)
+ provider.is_a?(RDocDocumentProvider) ? provider.available? : true
end
end
@@ -375,7 +367,7 @@ def show_doc_dialog_proc
when CommandDocument
input_method.command_doc_dialog_contents(target.name, width)
when MethodDocument
- input_method.rdoc_dialog_contents(target.name, width)
+ input_method.doc_dialog_contents(target.name, width)
else
if show_easter_egg
input_method.easter_egg_dialog_contents
@@ -403,52 +395,38 @@ def easter_egg_dialog_contents
lines
end
- def rdoc_dialog_contents(name, width)
- formatter = RDoc::Markup::ToAnsi.new
- formatter.width = width
+ # Asks IRB.doc_providers, in order, for the dialog preview of +name+.
+ def doc_dialog_contents(name, width)
+ IRB.doc_providers.each do |provider|
+ next unless provider.respond_to?(:dialog_contents)
- begin
- document = retrieve_rdoc_document(name)
- rescue RDoc::RI::Driver::NotFoundError
- return
- rescue => e
- raise if $DEBUG
- return rdoc_error_document(e).accept(formatter).split("\n")
+ begin
+ contents = provider.dialog_contents(name, width)
+ rescue => e
+ raise if $DEBUG
+ return doc_error_dialog_contents(e)
+ end
+ return [PRESS_ALT_D_TO_READ_FULL_DOC] + contents if contents
end
- return unless document
-
- [PRESS_ALT_D_TO_READ_FULL_DOC] + document.accept(formatter).split("\n")
+ nil
end
- def retrieve_rdoc_document(name)
- driver = rdoc_ri_driver
- return unless driver
-
- name = driver.expand_name(name)
-
- if name =~ /#|\./
- d = RDoc::Markup::Document.new
- driver.add_method(d, name)
- d
- else
- found, klasses, includes, extends = driver.classes_and_includes_and_extends_for(name)
- if found.empty?
- d = RDoc::Markup::Document.new
- driver.add_method(d, name)
- d
- else
- driver.class_document(name, found, klasses, includes, extends)
- end
- end
+ def doc_error_dialog_contents(error)
+ [
+ "Failed to load the document:",
+ "#{error.class}: #{error.message}",
+ "",
+ "Restart IRB with -d to see the backtrace.",
+ ]
end
- def rdoc_error_document(error)
- document = RDoc::Markup::Document.new
- document << RDoc::Markup::Paragraph.new("Failed to load the document:")
- document << RDoc::Markup::Paragraph.new("#{error.class}: #{error.message}")
- document << RDoc::Markup::BlankLine.new
- document << RDoc::Markup::Paragraph.new("Restart IRB with -d to see the backtrace.")
- document
+ # Asks IRB.doc_providers, in order, for the documentation of +name+.
+ def retrieve_document(name)
+ IRB.doc_providers.each do |provider|
+ document = provider.document(name)
+ return document if document
+ end
+ nil
end
def dialog_doc_position(cursor_pos_to_render, autocomplete_dialog, screen_width)
@@ -496,28 +474,17 @@ def display_document(matched)
end
end
when MethodDocument
- driver = rdoc_ri_driver
- return unless driver
-
if matched =~ /\A(?:::)?RubyVM/ && !ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER']
IRB.__send__(:easter_egg)
return
end
- if target.names.length > 1
- out = RDoc::Markup::Document.new
- target.names.each do |m|
- begin
- driver.add_method(out, m)
- rescue RDoc::RI::Driver::NotFoundError
- end
- end
- driver.display(out)
- else
- begin
- driver.display_names([target.name])
- rescue RDoc::RI::Driver::NotFoundError
- end
+ # An ambiguous receiver has several candidate names; show all of them.
+ documents = target.names.filter_map { |name| retrieve_document(name) }
+ return if documents.empty?
+
+ Pager.page(retain_content: true) do |io|
+ io.puts documents.join("\n")
end
end
end
diff --git a/test/irb/helper.rb b/test/irb/helper.rb
index 81ae5892f..8a161a106 100644
--- a/test/irb/helper.rb
+++ b/test/irb/helper.rb
@@ -84,6 +84,14 @@ def with_default_external(encoding)
EnvUtil.suppress_warning { Encoding.default_external = original }
end
+ def with_doc_providers(*providers)
+ original = IRB.doc_providers.dup
+ IRB.doc_providers.replace(providers)
+ yield
+ ensure
+ IRB.doc_providers.replace(original)
+ end
+
def without_rdoc(&block)
::Kernel.send(:alias_method, :irb_original_require, :require)
diff --git a/test/irb/test_command.rb b/test/irb/test_command.rb
index 1a99bc7eb..5758d16f4 100644
--- a/test/irb/test_command.rb
+++ b/test/irb/test_command.rb
@@ -874,6 +874,63 @@ def test_show_doc_without_rdoc
# this is the only way to reset the redefined method without coupling the test with its implementation
EnvUtil.suppress_warning { load "irb/command/help.rb" }
end
+
+ class StubDocProvider
+ def initialize(documents)
+ @documents = documents
+ end
+
+ def document(name)
+ @documents[name]
+ end
+ end
+
+ def test_show_doc_with_doc_provider
+ provider = StubDocProvider.new("Foo#bar" => "documentation of Foo#bar")
+
+ out, err = with_doc_providers(provider, IRB::RDocDocumentProvider.new) do
+ execute_lines("show_doc Foo#bar")
+ end
+
+ assert_empty(err)
+ assert_include(out, "documentation of Foo#bar")
+ end
+
+ def test_show_doc_with_doc_provider_without_rdoc
+ provider = StubDocProvider.new("Foo#bar" => "documentation of Foo#bar")
+
+ out, err = without_rdoc do
+ with_doc_providers(provider, IRB::RDocDocumentProvider.new) do
+ execute_lines("show_doc Foo#bar")
+ end
+ end
+
+ assert_empty(err)
+ assert_include(out, "documentation of Foo#bar")
+ end
+
+ if HAS_RDOC
+ def test_show_doc_falls_back_to_the_next_provider
+ provider = StubDocProvider.new({})
+
+ out, err = with_doc_providers(provider, IRB::RDocDocumentProvider.new) do
+ execute_lines("show_doc String#gsub")
+ end
+
+ assert_empty(err)
+ possible_rdoc_output = [/Nothing known about String#gsub/, /gsub\(pattern\)/]
+ assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the `show_doc` command to match one of the possible outputs. Got:\n#{out}")
+ end
+
+ def test_show_doc_reports_unknown_name
+ out, err = with_doc_providers(StubDocProvider.new({})) do
+ execute_lines("show_doc Foo#bar")
+ end
+
+ assert_empty(err)
+ assert_include(out, "Nothing known about Foo#bar")
+ end
+ end
end
class EditTest < CommandTestCase
diff --git a/test/irb/test_doc_provider.rb b/test/irb/test_doc_provider.rb
new file mode 100644
index 000000000..99336edfa
--- /dev/null
+++ b/test/irb/test_doc_provider.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: false
+require "irb"
+begin
+ require "rdoc"
+rescue LoadError
+end
+require_relative "helper"
+
+module TestIRB
+ class DocProviderTest < TestCase
+ def setup
+ @conf_backup = IRB.conf.dup
+ IRB.init_config(nil)
+ end
+
+ def teardown
+ IRB.conf.replace(@conf_backup)
+ end
+
+ def test_default_providers
+ assert_kind_of(Array, IRB.doc_providers)
+ assert(IRB.doc_providers.any? { |provider| provider.is_a?(IRB::RDocDocumentProvider) })
+ end
+ end
+
+ class RDocDocumentProviderTest < TestCase
+ def setup
+ @conf_backup = IRB.conf.dup
+ IRB.init_config(nil)
+ @provider = IRB::RDocDocumentProvider.new
+ end
+
+ def teardown
+ IRB.conf.replace(@conf_backup)
+ end
+
+ def test_available
+ assert(@provider.available?)
+ without_rdoc do
+ refute(@provider.available?)
+ end
+ end
+
+ def test_document_returns_nil_without_rdoc
+ without_rdoc do
+ assert_nil(@provider.document("String#gsub"))
+ assert_nil(@provider.dialog_contents("String.gsub", 40))
+ end
+ end
+
+ def test_document_returns_nil_for_unknown_name
+ assert_nil(@provider.document("Foo#bar"))
+ assert_nil(@provider.dialog_contents("Foo.bar", 40))
+ end
+
+ def test_not_found_message_for_unknown_name
+ assert_equal("Nothing known about Foo#bar", @provider.not_found_message("Foo#bar"))
+ assert_equal("Nothing known about Foo", @provider.not_found_message("Foo"))
+ end
+
+ def test_not_found_message_without_ri_data
+ provider = IRB::RDocDocumentProvider.new
+ provider.instance_variable_set(:@driver, RDoc::RI::Driver.new(use_system: false, use_site: false, use_home: false, use_gems: false))
+
+ assert_equal("Nothing known about String#gsub", provider.not_found_message("String#gsub"))
+ assert_equal("Nothing known about String", provider.not_found_message("String"))
+ end
+
+ def test_document
+ omit "This test requires RI data" unless has_rdoc_content?
+
+ document = @provider.document("Array.new")
+ assert_include(strip_formatting(document), "Array.new")
+
+ document = @provider.document("String")
+ assert_include(strip_formatting(document), "String < Object")
+ end
+
+ def test_dialog_contents
+ omit "This test requires RI data" unless has_rdoc_content?
+
+ contents = @provider.dialog_contents("Array.new", 40)
+ assert_kind_of(Array, contents)
+ assert_include(strip_formatting(contents.join("\n")), "Array.new")
+ end
+
+ def test_not_found_message_suggests_similar_names
+ omit "This test requires RI data" unless has_rdoc_content?
+
+ message = @provider.not_found_message("String#gsu")
+ assert_include(message, "maybe you meant")
+ assert_include(message, "String#gsub")
+
+ assert_include(@provider.not_found_message("String#zzz"), "Nothing known about String#zzz")
+ end
+
+ private
+
+ def has_rdoc_content?
+ File.exist?(RDoc::RI::Paths::BASE)
+ end
+
+ # remove the bold formatting of RDoc::Markup::ToBs and RDoc::Markup::ToAnsi
+ def strip_formatting(text)
+ text.gsub(/.\x08/, "").gsub(/\e\[[0-9;]*m/, "")
+ end
+ end if defined?(RDoc)
+end
diff --git a/test/irb/test_input_method.rb b/test/irb/test_input_method.rb
index d412e9e81..924d3a4d2 100644
--- a/test/irb/test_input_method.rb
+++ b/test/irb/test_input_method.rb
@@ -105,16 +105,10 @@ def test_initialization_with_use_autocomplete_but_without_rdoc
end
class DisplayDocumentTest < InputMethodTest
- def setup
- super
- @driver = RDoc::RI::Driver.new(use_stdout: true)
- end
-
- def display_document(target, bind, driver = nil)
+ def display_document(target, bind)
use_pager = IRB.conf[:USE_PAGER]
IRB.conf[:USE_PAGER] = false
input_method = IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
- input_method.instance_variable_set(:@rdoc_ri_driver, driver) if driver
input_method.instance_variable_set(:@completion_params, ['', target, '', bind])
input_method.display_document(target)
ensure
@@ -125,7 +119,7 @@ def test_perfectly_matched_namespace_triggers_document_display
omit unless has_rdoc_content?
out, err = capture_output do
- display_document("String", binding, @driver)
+ display_document("String", binding)
end
assert_empty(err)
@@ -136,7 +130,7 @@ def test_perfectly_matched_namespace_triggers_document_display
def test_perfectly_matched_multiple_namespaces_triggers_document_display
result = nil
out, err = capture_output do
- result = display_document("{}.nil?", binding, @driver)
+ result = display_document("{}.nil?", binding)
end
assert_empty(err)
@@ -148,17 +142,16 @@ def test_perfectly_matched_multiple_namespaces_triggers_document_display
assert_include(out, "P\bPr\bro\boc\bc.\b.n\bni\bil\bl?\b?") # Proc.nil?
assert_include(out, "H\bHa\bas\bsh\bh.\b.n\bni\bil\bl?\b?") # Hash.nil?
else
- # this is a hacky way to verify the rdoc rendering code path because CI doesn't have rdoc content
- # if there are multiple namespaces to be rendered, PerfectMatchedProc renders the result with a document
- # which always returns the bytes rendered, even if it's 0
- assert_equal(0, result)
+ # nothing is displayed when no documentation is available
+ assert_empty(out)
+ assert_nil(result)
end
end
def test_not_matched_namespace_triggers_nothing
result = nil
out, err = capture_output do
- result = display_document("Stri", binding, @driver)
+ result = display_document("Stri", binding)
end
assert_empty(err)
@@ -183,7 +176,7 @@ def test_perfect_matching_stops_without_rdoc
def test_perfect_matching_handles_nil_namespace
out, err = capture_output do
# symbol literal has `nil` doc namespace so it's a good test subject
- assert_nil(display_document(":aiueo", binding, @driver))
+ assert_nil(display_document(":aiueo", binding))
end
assert_empty(err)
@@ -208,6 +201,48 @@ def test_command_doc_display_without_help_message
assert_include(out, IRB::Command::History.description)
end
+ def test_documents_of_all_candidate_names_are_displayed
+ provider = StubDocProvider.new("Hash.any?" => "doc of Hash#any?", "Proc.any?" => "doc of Proc#any?")
+
+ out, err = capture_output do
+ with_doc_providers(provider) do
+ display_document("{}.any?", binding)
+ end
+ end
+
+ assert_empty(err)
+ assert_include(out, "doc of Hash#any?")
+ assert_include(out, "doc of Proc#any?")
+ end
+
+ def test_providers_are_consulted_in_order
+ first = StubDocProvider.new({})
+ second = StubDocProvider.new("String" => "doc of String")
+
+ out, err = capture_output do
+ with_doc_providers(first, second) do
+ display_document("String", binding)
+ end
+ end
+
+ assert_empty(err)
+ assert_equal(["String"], first.requested_names)
+ assert_include(out, "doc of String")
+ end
+
+ def test_nothing_is_displayed_when_no_provider_knows_the_name
+ result = nil
+ out, err = capture_output do
+ with_doc_providers(StubDocProvider.new({})) do
+ result = display_document("String", binding)
+ end
+ end
+
+ assert_empty(err)
+ assert_empty(out)
+ assert_nil(result)
+ end
+
private
def has_rdoc_content?
@@ -215,13 +250,135 @@ def has_rdoc_content?
end
end if defined?(RDoc)
+ class StubDocProvider
+ attr_reader :requested_names
+
+ def initialize(documents)
+ @documents = documents
+ @requested_names = []
+ end
+
+ def document(name)
+ @requested_names << name
+ @documents[name]
+ end
+
+ def dialog_contents(name, width)
+ @requested_names << name
+ document = @documents[name]
+ ["#{name} (width: #{width})", document] if document
+ end
+ end
+
+ class DocumentOnlyProvider
+ def initialize(documents)
+ @documents = documents
+ end
+
+ def document(name)
+ @documents[name]
+ end
+ end
+
+ class DocDialogContentsTest < InputMethodTest
+ def test_providers_are_consulted_in_order
+ first = StubDocProvider.new({})
+ second = StubDocProvider.new("String.gsub" => "doc of String#gsub")
+
+ contents = with_doc_providers(first, second) do
+ build_input_method.doc_dialog_contents("String.gsub", 40)
+ end
+
+ assert_equal(["String.gsub"], first.requested_names)
+ assert_equal([IRB::RelineInputMethod::PRESS_ALT_D_TO_READ_FULL_DOC, "String.gsub (width: 40)", "doc of String#gsub"], contents)
+ end
+
+ def test_providers_without_dialog_contents_are_skipped
+ provider = DocumentOnlyProvider.new("String.gsub" => "doc of String#gsub")
+
+ contents = with_doc_providers(provider) do
+ build_input_method.doc_dialog_contents("String.gsub", 40)
+ end
+
+ assert_nil(contents)
+ end
+
+ def test_returns_nil_when_no_provider_knows_the_name
+ contents = with_doc_providers(StubDocProvider.new({})) do
+ build_input_method.doc_dialog_contents("String.gsub", 40)
+ end
+
+ assert_nil(contents)
+ end
+
+ def test_shows_error_content_when_provider_raises
+ provider = Object.new
+ provider.define_singleton_method(:document) { |_name| nil }
+ provider.define_singleton_method(:dialog_contents) { |_name, _width| raise ArgumentError, "broken provider" }
+
+ contents = nil
+ assert_nothing_raised do
+ contents = with_doc_providers(provider) do
+ build_input_method.doc_dialog_contents("String.gsub", 40)
+ end
+ end
+
+ assert_include(contents, "ArgumentError: broken provider")
+ assert_include(contents, "Restart IRB with -d to see the backtrace.")
+ assert_not_include(contents, IRB::RelineInputMethod::PRESS_ALT_D_TO_READ_FULL_DOC)
+ end
+
+ def test_dialog_is_registered_when_a_provider_supports_it_without_rdoc
+ original_show_doc_proc = Reline.dialog_proc(:show_doc)&.dialog_proc
+ empty_proc = Proc.new {}
+ Reline.add_dialog_proc(:show_doc, empty_proc)
+ IRB.conf[:USE_AUTOCOMPLETE] = true
+
+ without_rdoc do
+ with_doc_providers(StubDocProvider.new({}), IRB::RDocDocumentProvider.new) do
+ IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
+ end
+ end
+
+ assert_not_equal empty_proc, Reline.dialog_proc(:show_doc).dialog_proc
+ ensure
+ Reline.add_dialog_proc(:show_doc, original_show_doc_proc, Reline::DEFAULT_DIALOG_CONTEXT)
+ end
+
+ def test_dialog_is_not_registered_when_no_provider_supports_it
+ original_show_doc_proc = Reline.dialog_proc(:show_doc)&.dialog_proc
+ empty_proc = Proc.new {}
+ Reline.add_dialog_proc(:show_doc, empty_proc)
+ IRB.conf[:USE_AUTOCOMPLETE] = true
+
+ with_doc_providers(DocumentOnlyProvider.new({})) do
+ IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
+ end
+
+ assert_equal empty_proc, Reline.dialog_proc(:show_doc).dialog_proc
+ ensure
+ Reline.add_dialog_proc(:show_doc, original_show_doc_proc, Reline::DEFAULT_DIALOG_CONTEXT)
+ end
+
+ private
+
+ def build_input_method
+ IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
+ end
+ end
+
class RdocDialogContentsTest < InputMethodTest
+ def teardown
+ IRB.doc_providers.replace(@original_providers) if @original_providers
+ super
+ end
+
def test_shows_error_content_when_document_retrieval_raises
input_method = build_input_method(failing_driver(ArgumentError.new("undefined class/module RDoc::")))
contents = nil
assert_nothing_raised do
- contents = input_method.rdoc_dialog_contents("1.foo", 40)
+ contents = input_method.doc_dialog_contents("1.foo", 40)
end
assert_not_nil contents
@@ -243,7 +400,7 @@ def test_raises_the_error_when_debug_is_enabled
# $DEBUG makes Ruby print raised exceptions to stderr; swallow that noise.
capture_output do
assert_raise(ArgumentError) do
- input_method.rdoc_dialog_contents("1.foo", 40)
+ input_method.doc_dialog_contents("1.foo", 40)
end
end
ensure
@@ -253,7 +410,7 @@ def test_raises_the_error_when_debug_is_enabled
def test_includes_full_document_hint_when_document_is_available
input_method = build_input_method(documented_driver)
- contents = input_method.rdoc_dialog_contents("1.foo", 40)
+ contents = input_method.doc_dialog_contents("1.foo", 40)
assert_equal IRB::RelineInputMethod::PRESS_ALT_D_TO_READ_FULL_DOC, contents.first
end
@@ -261,15 +418,17 @@ def test_includes_full_document_hint_when_document_is_available
def test_returns_nil_when_document_not_found
input_method = build_input_method(failing_driver(RDoc::RI::Driver::NotFoundError.new("1.foo")))
- assert_nil input_method.rdoc_dialog_contents("1.foo", 40)
+ assert_nil input_method.doc_dialog_contents("1.foo", 40)
end
private
def build_input_method(driver)
- input_method = IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
- input_method.instance_variable_set(:@rdoc_ri_driver, driver)
- input_method
+ provider = IRB::RDocDocumentProvider.new
+ provider.instance_variable_set(:@driver, driver)
+ @original_providers = IRB.doc_providers.dup
+ IRB.doc_providers.replace([provider])
+ IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
end
def failing_driver(error)