Skip to content

Proposal: public hook for external documentation providers in show_doc and the completion doc dialog #1243

Description

@znz

Summary

show_doc and the Alt+d documentation dialog shown by autocompletion are hard-wired to RDoc::RI::Driver. This proposes a small public extension point, a list of "document providers", so that other documentation backends (RI-compatible or not) can plug into the same UI without monkey-patching irb internals.

Motivation

irb currently has exactly one way to influence where documentation comes from: IRB.conf[:EXTRA_DOC_DIRS], which only adds more RI data directories. There is no way to serve documentation from a different format or source (a different language's manual, YARD-generated docs, RBS-embedded comments, a project-local doc store, etc.) through show_doc or the completion dialog.

Rurema (the Japanese Ruby reference manual) ships bitclust-irb, added in rurema/bitclust#326 (merged 2026-08-20, released in bitclust 1.7.0). Because there was no hook into show_doc, it had to register an entirely separate refe command through the public IRB::Command.register API instead of extending show_doc itself. A follow-up PR (rurema/bitclust#332) adds a fallback that searches docs.ruby-lang.org (its search index plus the Markdown pages) when no local database is present. Users have to remember two commands (show_doc for RI, refe for the Japanese manual) and only one of them gets the Alt+d dialog treatment.

A provider hook would let show_doc NAME and the completion dialog consult multiple backends in order, with RI as the default, so bitclust-irb (and similarly YARD's yri, or other translated manuals) could integrate directly instead of bolting on a parallel command.

Current implementation

lib/irb/command/show_doc.rb (ShowDoc#execute) always does:

require 'rdoc/ri/driver'
opts = RDoc::RI::Driver.process_args([])
ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts))
...
Ri.display_name(name)   # or Ri.interactive when name is nil

and warns "Can't display document because rdoc is not installed." when rdoc can't be required.

lib/irb/input-method.rb (RelineInputMethod) independently drives the same backend for the Alt+d dialog:

  • rdoc_ri_driver builds an RDoc::RI::Driver.new(options), honoring IRB.conf[:EXTRA_DOC_DIRS].
  • retrieve_rdoc_document(name) calls driver.expand_name(name) then driver.add_method / driver.class_document to build an RDoc::Markup::Document.
  • rdoc_dialog_contents(name, width) renders that document with RDoc::Markup::ToAnsi for the popup.
  • display_document(matched) handles the Alt+d full-screen view: CommandDocument targets go through IRB::Command.load_command, MethodDocument targets go through the RI driver's display_names (or add_method + display when there are several candidate names, e.g. ambiguous receivers like {}.any?).
  • The whole dialog proc is only installed when require 'rdoc' succeeds (start in RelineInputMethod).

lib/irb/completion.rb supplies the names passed to the above. DocumentTarget, CommandDocument, and MethodDocument (MethodDocument#names can hold more than one name for an ambiguous receiver) came from #1180 ("Display command description in doc dialog on tab completion", merged 2026-03-13); rdoc_error_document for failed lookups was added by #1229 ("Keep completion alive when RDoc document retrieval fails", 2026-07-16). The names themselves are RI-style: the regexp completor emits things like "String.gsub" (a dot even for instance methods: RI's expand_name resolves it), "Array.new", or ["Hash.any?", "Proc.any?"] for an ambiguous {}.any?; the type-based completor gets the same shape from ReplTypeCompletor#doc_namespace. show_doc itself accepts anything RI accepts (Array, Array#each, Array.new, Array::new).

Proposal

Add a small ordered registry of document providers, defaulting to just the existing RDoc/RI behavior so nothing changes out of the box:

IRB.doc_providers                              # => [IRB::RDocDocumentProvider.new]
IRB.doc_providers.unshift(MyProvider.new)       # e.g. in ~/.irbrc; earlier providers win

(An alternative shape would be IRB.conf[:DOC_PROVIDERS], consistent with EXTRA_DOC_DIRS; either works, but a plain array with push/unshift seems simpler to use and to reason about ordering with.)

A provider is a duck type, no base class required:

class MyProvider
  # name is whatever show_doc / the completor already produce today (RI-style
  # names such as "String#gsub", "String.gsub", "Array.new", "Array"). Return
  # a String to be shown via IRB::Pager, or nil if this provider has nothing
  # for the name, so the next provider gets a chance.
  def document(name) end

  # Optional. A short preview for the completion dialog: an array of lines
  # that fit within `width` columns (ANSI escapes allowed). Return nil to
  # skip the dialog for this name. Providers may omit this method entirely.
  def dialog_contents(name, width) end
end

Resolution: providers are asked in order, and the first non-nil document/dialog_contents result wins. show_doc with no argument would keep starting RI's interactive session directly (providers are not consulted for that case, since "interactive" is RI-specific). When no provider returns anything, today's "not found" / "rdoc not installed" messages are kept.

The built-in IRB::RDocDocumentProvider would just be the existing code moved behind this interface: document wrapping Ri.display_name (captured instead of printed directly), dialog_contents wrapping retrieve_rdoc_document + RDoc::Markup::ToAnsi. So most of this is a refactor, not new behavior. A MethodDocument with multiple candidate names (ambiguous receivers) can be handled by calling document/dialog_contents once per name and combining, same as display_document does today with driver.add_method in a loop.

Sketch of ShowDoc#execute after the change, just to illustrate the shape (not final):

def execute(arg)
  name = unwrap_string_literal(arg)
  if name.nil?
    # unchanged: still delegates straight to RI's interactive session
    IRB::RDocDocumentProvider.new.interactive
    return
  end
  IRB.doc_providers.each do |provider|
    if (doc = provider.document(name))
      Pager.page_content(doc)
      return
    end
  end
  # not found: keep today's messages (RI's "Nothing known about ...")
end

With this in place, bitclust-irb could register a provider instead of a separate refe command, and show_doc String#gsub would show the Japanese manual page when available, falling back to RI.

Notes / open questions

  • Naming: IRB.doc_providers vs. IRB.conf[:DOC_PROVIDERS], and the provider method names (document/dialog_contents vs. something else), are open to bikeshedding.
  • Whether show_doc with no argument (RI's interactive mode) should also become pluggable, or stay RI-only as sketched above.
  • This is related to, but does not by itself solve, Consider letting help fall back to RI documentation #1242 ("Consider letting help fall back to RI documentation"): a provider abstraction would let help fall back to "documentation from some provider" rather than specifically RI, but that's a separate change to the help command.
  • Other plausible providers besides bitclust-irb: YARD's yri, RBS-embedded documentation, project-local documentation, or manuals translated into other languages.
  • I'm happy to send a PR implementing this if the direction looks acceptable to maintainers; bitclust-irb would be the first external consumer.

Related

日本語要約

show_doc コマンドおよび自動補完の Alt+d ドキュメントダイアログは RDoc::RI::Driver に直結しており、他のドキュメント源(翻訳マニュアルや YARD など)を差し込む手段がありません。本 issue は、RI をデフォルトとしつつ外部の「ドキュメントプロバイダ」を登録できる小さな公開フック(IRB.doc_providers 案)を提案します。rurema 側では bitclust-irb(rurema/bitclust#326)が該当フックの不在によりやむを得ず別コマンド refe を登録している経緯があり、本提案が採用されれば show_doc から直接日本語マニュアルを引けるようになります。実装の方向性が受け入れられるなら PR を送る用意があります。

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions