[ruby/irb] Fix nested IRB sessions' history saving

(https://github.com/ruby/irb/pull/652)

1. Dynamically including `HistorySavingAbility` makes things unnecessarily
   complicated and should be avoided.
2. Because both `Reline` and `Readline` use a single `HISTORY` constant
   to store history data. When nesting IRB sessions, only the first IRB
   session should handle history loading and saving so we can avoid
   duplicating history.
3. History saving callback should NOT be stored in `IRB.conf` as it's
   recreated every time `IRB.setup` is called, which would happen when
   nesting IRB sessions.

https://github.com/ruby/irb/commit/0fef0ae160
This commit is contained in:
Stan Lo 2023-08-09 15:57:47 +01:00 committed by git
parent 6acfc50bcc
commit ab0f90f1f5
5 changed files with 101 additions and 23 deletions

View File

@ -482,9 +482,16 @@ module IRB
end end
def run(conf = IRB.conf) def run(conf = IRB.conf)
in_nested_session = !!conf[:MAIN_CONTEXT]
conf[:IRB_RC].call(context) if conf[:IRB_RC] conf[:IRB_RC].call(context) if conf[:IRB_RC]
conf[:MAIN_CONTEXT] = context conf[:MAIN_CONTEXT] = context
save_history = !in_nested_session && conf[:SAVE_HISTORY] && context.io.support_history_saving?
if save_history
context.io.load_history
end
prev_trap = trap("SIGINT") do prev_trap = trap("SIGINT") do
signal_handle signal_handle
end end
@ -496,6 +503,7 @@ module IRB
ensure ensure
trap("SIGINT", prev_trap) trap("SIGINT", prev_trap)
conf[:AT_EXIT].each{|hook| hook.call} conf[:AT_EXIT].each{|hook| hook.call}
context.io.save_history if save_history
end end
end end

View File

@ -8,7 +8,6 @@ require_relative "workspace"
require_relative "inspector" require_relative "inspector"
require_relative "input-method" require_relative "input-method"
require_relative "output-method" require_relative "output-method"
require_relative "history"
module IRB module IRB
# A class that wraps the current state of the irb session, including the # A class that wraps the current state of the irb session, including the
@ -130,8 +129,6 @@ module IRB
else else
@io = input_method @io = input_method
end end
self.save_history = IRB.conf[:SAVE_HISTORY] if IRB.conf[:SAVE_HISTORY]
@extra_doc_dirs = IRB.conf[:EXTRA_DOC_DIRS] @extra_doc_dirs = IRB.conf[:EXTRA_DOC_DIRS]
@echo = IRB.conf[:ECHO] @echo = IRB.conf[:ECHO]
@ -154,13 +151,6 @@ module IRB
def save_history=(val) def save_history=(val)
IRB.conf[:SAVE_HISTORY] = val IRB.conf[:SAVE_HISTORY] = val
if val
context = (IRB.conf[:MAIN_CONTEXT] || self)
if context.io.support_history_saving? && !context.io.singleton_class.include?(HistorySavingAbility)
context.io.extend(HistorySavingAbility)
end
end
end end
def save_history def save_history

View File

@ -1,9 +1,7 @@
module IRB module IRB
module HistorySavingAbility # :nodoc: module HistorySavingAbility # :nodoc:
def HistorySavingAbility.extended(obj) def support_history_saving?
IRB.conf[:AT_EXIT].push proc{obj.save_history} true
obj.load_history
obj
end end
def load_history def load_history

View File

@ -5,6 +5,7 @@
# #
require_relative 'completion' require_relative 'completion'
require_relative "history"
require 'io/console' require 'io/console'
require 'reline' require 'reline'
@ -167,6 +168,8 @@ module IRB
include ::Readline include ::Readline
end end
include HistorySavingAbility
# Creates a new input method object using Readline # Creates a new input method object using Readline
def initialize def initialize
self.class.initialize_readline self.class.initialize_readline
@ -219,10 +222,6 @@ module IRB
true true
end end
def support_history_saving?
true
end
# Returns the current line number for #io. # Returns the current line number for #io.
# #
# #line counts the number of times #gets is called. # #line counts the number of times #gets is called.
@ -250,6 +249,7 @@ module IRB
class RelineInputMethod < InputMethod class RelineInputMethod < InputMethod
HISTORY = Reline::HISTORY HISTORY = Reline::HISTORY
include HistorySavingAbility
# Creates a new input method object using Reline # Creates a new input method object using Reline
def initialize def initialize
IRB.__send__(:set_encoding, Reline.encoding_system_needs.name, override: false) IRB.__send__(:set_encoding, Reline.encoding_system_needs.name, override: false)
@ -451,10 +451,6 @@ module IRB
str += " and #{inputrc_path}" if File.exist?(inputrc_path) str += " and #{inputrc_path}" if File.exist?(inputrc_path)
str str
end end
def support_history_saving?
true
end
end end
class ReidlineInputMethod < RelineInputMethod class ReidlineInputMethod < RelineInputMethod

View File

@ -1,9 +1,12 @@
# frozen_string_literal: false # frozen_string_literal: false
require 'irb' require 'irb'
require 'readline' require 'readline'
require "tempfile"
require_relative "helper" require_relative "helper"
return if RUBY_PLATFORM.match?(/solaris|mswin|mingw/i)
module TestIRB module TestIRB
class HistoryTest < TestCase class HistoryTest < TestCase
def setup def setup
@ -205,4 +208,87 @@ module TestIRB
end end
end end
end end
end if not RUBY_PLATFORM.match?(/solaris|mswin|mingw/i)
class NestedIRBHistoryTest < IntegrationTestCase
def test_history_saving_with_nested_sessions
write_history ""
write_ruby <<~'RUBY'
def foo
binding.irb
end
binding.irb
RUBY
run_ruby_file do
type "'outer session'"
type "foo"
type "'inner session'"
type "exit"
type "'outer session again'"
type "exit"
end
assert_equal <<~HISTORY, @history_file.open.read
'outer session'
foo
'inner session'
exit
'outer session again'
exit
HISTORY
end
def test_history_saving_with_nested_sessions_and_prior_history
write_history <<~HISTORY
old_history_1
old_history_2
old_history_3
HISTORY
write_ruby <<~'RUBY'
def foo
binding.irb
end
binding.irb
RUBY
run_ruby_file do
type "'outer session'"
type "foo"
type "'inner session'"
type "exit"
type "'outer session again'"
type "exit"
end
assert_equal <<~HISTORY, @history_file.open.read
old_history_1
old_history_2
old_history_3
'outer session'
foo
'inner session'
exit
'outer session again'
exit
HISTORY
end
private
def write_history(history)
@history_file = Tempfile.new('irb_history')
@history_file.write(history)
@history_file.close
@irbrc = Tempfile.new('irbrc')
@irbrc.write <<~RUBY
IRB.conf[:HISTORY_FILE] = "#{@history_file.path}"
RUBY
@irbrc.close
@envs['IRBRC'] = @irbrc.path
end
end
end