diff --git a/lib/irb/cmd/show_cmds.rb b/lib/irb/cmd/show_cmds.rb index 490561825..28efdcb60 100644 --- a/lib/irb/cmd/show_cmds.rb +++ b/lib/irb/cmd/show_cmds.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "stringio" +require "rdoc/ri/driver" require_relative "nop" module IRB @@ -28,9 +29,30 @@ def execute(*args) output.puts end - puts output.string + display_content(output.string) + end - nil + private + + def display_content(content) + if STDIN.tty? + begin + pid = nil + RDoc::RI::Driver.new.page do |io| + pid = io.pid + io.puts content + end + # When user presses Ctrl-C, IRB would raise `IRB::Abort` + # But because RDoc's pager is implemented by running `pager` or `less` in another process with `IO.popen`, + # the `IRB::Abort` exception only interrupts IRB's execution but doesn't affect the pager + # So to properly terminate the pager with Ctrl-C, we need to catch `IRB::Abort` and kill the pager process + rescue IRB::Abort + Process.kill("TERM", pid) if pid + nil + end + else + puts content + end end end end diff --git a/test/irb/yamatanooroti/test_rendering.rb b/test/irb/yamatanooroti/test_rendering.rb index d2342d6a2..12467b091 100644 --- a/test/irb/yamatanooroti/test_rendering.rb +++ b/test/irb/yamatanooroti/test_rendering.rb @@ -252,6 +252,24 @@ def test_assignment_expression_truncate EOC end + def test_show_cmds_with_pager_can_quit_with_ctrl_c + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("show_cmds\n") + write("G") # move to the end of the screen + write("\C-c") # quit pager + write("'foo' + 'bar'\n") # eval something to make sure IRB resumes + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + # IRB::Abort should be rescued + assert_not_match(/IRB::Abort/, screen) + # IRB should resume + assert_match(/foobar/, screen) + end + private def write_irbrc(content)