diff --git a/lib/irb.rb b/lib/irb.rb
index 86a1b6f6b..8857c34e6 100644
--- a/lib/irb.rb
+++ b/lib/irb.rb
@@ -98,14 +98,15 @@ class << self
# The lexer used by this irb session
attr_accessor :scanner
- attr_reader :from_binding
+ # Whether this session was started by Binding#irb
+ def from_binding
+ @context.from_binding?
+ end
# Creates a new irb session
def initialize(workspace = nil, input_method = nil, from_binding: false)
- @from_binding = from_binding
@prompt_part_cache = nil
- @context = Context.new(self, workspace, input_method)
- @context.workspace.load_helper_methods_to_main
+ @context = Context.new(self, workspace, input_method, from_binding: from_binding)
@signal_status = :IN_IRB
@scanner = RubyLex.new
@line_no = 1
@@ -124,9 +125,7 @@ def debug_break
end
def debug_readline(binding)
- workspace = IRB::WorkSpace.new(binding)
- context.replace_workspace(workspace)
- context.workspace.load_helper_methods_to_main
+ context.replace_workspace(IRB::WorkSpace.new(binding))
@line_no += 1
# When users run:
@@ -489,12 +488,8 @@ def suspend_name(path = nil, name = nil)
# Context#workspace.
#
# Used by the irb command `irb_load`, see IRB@IRB+Sessions for more information.
- def suspend_workspace(workspace)
- current_workspace = @context.workspace
- @context.replace_workspace(workspace)
- yield
- ensure
- @context.replace_workspace current_workspace
+ def suspend_workspace(workspace, &block)
+ @context.workspaces.with(workspace, &block)
end
# Evaluates the given block using the given `input_method` as the Context#io.
@@ -503,11 +498,11 @@ def suspend_workspace(workspace)
# more information.
def suspend_input_method(input_method)
back_io = @context.io
- @context.instance_eval{@io = input_method}
+ @context.io = input_method
begin
yield back_io
ensure
- @context.instance_eval{@io = back_io}
+ @context.io = back_io
end
end
diff --git a/lib/irb/command/chws.rb b/lib/irb/command/chws.rb
index ef456d096..f63f35e59 100644
--- a/lib/irb/command/chws.rb
+++ b/lib/irb/command/chws.rb
@@ -3,7 +3,6 @@
# change-ws.rb -
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-require_relative "../ext/change-ws"
module IRB
# :stopdoc:
diff --git a/lib/irb/command/measure.rb b/lib/irb/command/measure.rb
index 47f9cf47f..8bcfcce46 100644
--- a/lib/irb/command/measure.rb
+++ b/lib/irb/command/measure.rb
@@ -8,10 +8,6 @@ class Measure < Base
category "Misc"
description "`measure` enables the mode to measure processing time. `measure :off` disables it."
- def initialize(*args)
- super(*args)
- end
-
def execute(arg)
if arg&.match?(/^do$|^do[^\w]|^\{/)
warn 'Configure IRB.conf[:MEASURE_PROC] to add custom measure methods.'
diff --git a/lib/irb/command/pushws.rb b/lib/irb/command/pushws.rb
index 1c72897d3..f18192e1f 100644
--- a/lib/irb/command/pushws.rb
+++ b/lib/irb/command/pushws.rb
@@ -4,8 +4,6 @@
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-require_relative "../ext/workspaces"
-
module IRB
# :stopdoc:
@@ -15,7 +13,7 @@ class Workspaces < Base
description "Show workspaces."
def execute(_arg)
- inspection_results = irb_context.instance_variable_get(:@workspace_stack).map do |ws|
+ inspection_results = irb_context.workspaces.map do |ws|
truncated_inspect(ws.main)
end
diff --git a/lib/irb/context.rb b/lib/irb/context.rb
index 64bf6362a..02db88772 100644
--- a/lib/irb/context.rb
+++ b/lib/irb/context.rb
@@ -5,6 +5,7 @@
#
require_relative "workspace"
+require_relative "workspace_stack"
require_relative "inspector"
require_relative "input-method"
require_relative "output-method"
@@ -24,14 +25,10 @@ class Context
# +nil+:: uses stdin or Reline or Readline
# +String+:: uses a File
# +other+:: uses this as InputMethod
- def initialize(irb, workspace = nil, input_method = nil)
+ def initialize(irb, workspace = nil, input_method = nil, from_binding: false)
@irb = irb
- @workspace_stack = []
- if workspace
- @workspace_stack << workspace
- else
- @workspace_stack << WorkSpace.new
- end
+ @from_binding = from_binding
+ @workspaces = WorkspaceStack.new(workspace || WorkSpace.new)
@thread = Thread.current
# copy of default configuration
@@ -190,24 +187,61 @@ def history_file=(hist)
IRB.conf[:HISTORY_FILE] = hist
end
+ # The stack of workspaces in this context, see WorkspaceStack.
+ attr_reader :workspaces
+
# Workspace in the current context.
def workspace
- @workspace_stack.last
+ @workspaces.current
+ end
+
+ # The workspace this context started with.
+ def home_workspace
+ @workspaces.home
end
# Replace the current workspace with the given +workspace+.
def replace_workspace(workspace)
- @workspace_stack.pop
- @workspace_stack.push(workspace)
+ @workspaces.replace(workspace)
+ end
+
+ # Changes the current workspace to given object or binding.
+ #
+ # If the optional argument is omitted, the workspace will be
+ # #home_workspace.
+ #
+ # See IRB::WorkSpace.new for more information.
+ def change_workspace(*_main)
+ if _main.empty?
+ replace_workspace(home_workspace)
+ return main
+ end
+
+ replace_workspace(WorkSpace.new(_main[0]))
+ end
+
+ # Creates a new workspace with the given object, and pushes it onto the
+ # workspace stack. Without an argument, swaps the two topmost workspaces.
+ #
+ # See IRB::WorkSpace.new for more information.
+ def push_workspace(*_main)
+ if _main.empty?
+ @workspaces.swap
+ else
+ @workspaces.push(WorkSpace.new(workspace.binding, _main[0]))
+ end
+ end
+
+ # Removes the current workspace from the stack and returns it, or +nil+ if
+ # only the home workspace is left.
+ def pop_workspace
+ @workspaces.pop
end
# The top-level workspace, see WorkSpace#main
def main
workspace.main
end
-
- # The toplevel workspace, see #home_workspace
- attr_reader :workspace_home
# The current thread in this context.
attr_reader :thread
# The current input method.
@@ -565,8 +599,9 @@ def evaluate(statement, line_no) # :nodoc:
nil
end
+ # Whether this context was started by Binding#irb.
def from_binding?
- @irb.from_binding
+ @from_binding
end
def evaluate_expression(code, line_no) # :nodoc:
diff --git a/lib/irb/ext/change-ws.rb b/lib/irb/ext/change-ws.rb
deleted file mode 100644
index 60e8afe31..000000000
--- a/lib/irb/ext/change-ws.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-#
-# irb/ext/cb.rb -
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-
-module IRB # :nodoc:
- class Context
-
- # Inherited from +TOPLEVEL_BINDING+.
- def home_workspace
- if defined? @home_workspace
- @home_workspace
- else
- @home_workspace = workspace
- end
- end
-
- # Changes the current workspace to given object or binding.
- #
- # If the optional argument is omitted, the workspace will be
- # #home_workspace which is inherited from +TOPLEVEL_BINDING+ or the main
- # object, IRB.conf[:MAIN_CONTEXT] when irb was initialized.
- #
- # See IRB::WorkSpace.new for more information.
- def change_workspace(*_main)
- if _main.empty?
- replace_workspace(home_workspace)
- return main
- end
-
- workspace = WorkSpace.new(_main[0])
- replace_workspace(workspace)
- workspace.load_helper_methods_to_main
- end
- end
-end
diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb
index 0955b4acf..a7f0cadc0 100644
--- a/lib/irb/ext/loader.rb
+++ b/lib/irb/ext/loader.rb
@@ -92,33 +92,5 @@ def load_file(path, priv = nil)
end
end
end
-
- def old # :nodoc:
- back_io = @io
- back_path = irb_path
- back_name = @irb_name
- back_scanner = @irb.scanner
- begin
- @io = FileInputMethod.new(path)
- @irb_name = File.basename(path)
- self.irb_path = path
- @irb.signal_status(:IN_LOAD) do
- if back_io.kind_of?(FileInputMethod)
- @irb.eval_input
- else
- begin
- @irb.eval_input
- rescue LoadAbort
- print "load abort!!\n"
- end
- end
- end
- ensure
- @io = back_io
- @irb_name = back_name
- self.irb_path = back_path
- @irb.scanner = back_scanner
- end
- end
end
end
diff --git a/lib/irb/ext/workspaces.rb b/lib/irb/ext/workspaces.rb
deleted file mode 100644
index da09faa83..000000000
--- a/lib/irb/ext/workspaces.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-#
-# push-ws.rb -
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-
-module IRB # :nodoc:
- class Context
- # Creates a new workspace with the given object or binding, and appends it
- # onto the current #workspaces stack.
- #
- # See IRB::Context#change_workspace and IRB::WorkSpace.new for more
- # information.
- def push_workspace(*_main)
- if _main.empty?
- if @workspace_stack.size > 1
- # swap the top two workspaces
- previous_workspace, current_workspace = @workspace_stack.pop(2)
- @workspace_stack.push current_workspace, previous_workspace
- end
- else
- new_workspace = WorkSpace.new(workspace.binding, _main[0])
- @workspace_stack.push new_workspace
- new_workspace.load_helper_methods_to_main
- end
- end
-
- # Removes the last element from the current #workspaces stack and returns
- # it, or +nil+ if the current workspace stack is empty.
- #
- # Also, see #push_workspace.
- def pop_workspace
- @workspace_stack.pop if @workspace_stack.size > 1
- end
- end
-end
diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb
index 9fef8f86a..4281d0d42 100644
--- a/lib/irb/workspace.rb
+++ b/lib/irb/workspace.rb
@@ -9,22 +9,18 @@
IRB::TOPLEVEL_BINDING = binding
module IRB # :nodoc:
class WorkSpace
- # Creates a new workspace.
- #
- # set self to main if specified, otherwise
- # inherit main from TOPLEVEL_BINDING.
- def initialize(*main)
- if Binding === main[0]
- @binding = main.shift
- elsif IRB.conf[:SINGLE_IRB]
- @binding = TOPLEVEL_BINDING
- else
+ class << self
+ # The binding new workspaces are created from when none is given, as
+ # configured by IRB.conf[:CONTEXT_MODE].
+ def base_binding
+ return TOPLEVEL_BINDING if IRB.conf[:SINGLE_IRB]
+
case IRB.conf[:CONTEXT_MODE]
when 0 # binding in proc on TOPLEVEL_BINDING
- @binding = eval("proc{binding}.call",
- TOPLEVEL_BINDING,
- __FILE__,
- __LINE__)
+ eval("proc{binding}.call",
+ TOPLEVEL_BINDING,
+ __FILE__,
+ __LINE__)
when 1 # binding in loaded file
require "tempfile"
f = Tempfile.open("irb-binding")
@@ -33,7 +29,7 @@ def initialize(*main)
EOF
f.close
load f.path
- @binding = $binding
+ $binding
when 2 # binding in loaded file(thread use)
unless defined? BINDING_QUEUE
@@ -44,17 +40,17 @@ def initialize(*main)
end
Thread.pass
end
- @binding = BINDING_QUEUE.pop
+ BINDING_QUEUE.pop
when 3 # binding in function on TOPLEVEL_BINDING
- @binding = eval("self.class.remove_method(:irb_binding) if defined?(irb_binding); private; def irb_binding; binding; end; irb_binding",
- TOPLEVEL_BINDING,
- __FILE__,
- __LINE__ - 3)
+ eval("self.class.remove_method(:irb_binding) if defined?(irb_binding); private; def irb_binding; binding; end; irb_binding",
+ TOPLEVEL_BINDING,
+ __FILE__,
+ __LINE__ - 3)
when 4 # binding is a copy of TOPLEVEL_BINDING (default)
# Note that this will typically be IRB::TOPLEVEL_BINDING
# This is to avoid RubyGems' local variables (see issue #17623)
- @binding = TOPLEVEL_BINDING.dup
+ TOPLEVEL_BINDING.dup
when 5 # binding in Ruby::Box
unless defined?(Ruby::Box)
@@ -63,9 +59,17 @@ def initialize(*main)
end
puts 'Context-mode 5 (binding in Ruby::Box) is experimental. It may be removed or changed without notice.'
- @binding = Ruby::Box.new.eval('Kernel.binding')
+ Ruby::Box.new.eval('Kernel.binding')
end
end
+ end
+
+ # Creates a new workspace.
+ #
+ # set self to main if specified, otherwise
+ # inherit main from TOPLEVEL_BINDING.
+ def initialize(*main)
+ @binding = Binding === main[0] ? main.shift : self.class.base_binding
if main.empty?
@main = eval("self", @binding)
diff --git a/lib/irb/workspace_stack.rb b/lib/irb/workspace_stack.rb
new file mode 100644
index 000000000..b985ce1dc
--- /dev/null
+++ b/lib/irb/workspace_stack.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module IRB
+ # The stack of workspaces of an IRB session.
+ #
+ # The bottom workspace is the home workspace, which is fixed at creation. The
+ # top workspace is the current one, where input is evaluated. The stack is
+ # never empty.
+ #
+ # Every workspace that becomes current goes through this class, so this is the
+ # only place that loads helper methods into a workspace's main object.
+ class WorkspaceStack
+ include Enumerable
+
+ def initialize(home)
+ @stack = []
+ push(home)
+ end
+
+ # The workspace the session started with.
+ def home
+ @stack.first
+ end
+
+ # The workspace where input is evaluated.
+ def current
+ @stack.last
+ end
+
+ def size
+ @stack.size
+ end
+
+ def each(&block)
+ @stack.each(&block)
+ end
+
+ def push(workspace)
+ workspace.load_helper_methods_to_main
+ @stack.push(workspace)
+ workspace
+ end
+
+ # Removes and returns the current workspace. The home workspace is never
+ # removed, +nil+ is returned instead.
+ def pop
+ @stack.pop if @stack.size > 1
+ end
+
+ # Swaps the two topmost workspaces. Does nothing if there's only one.
+ def swap
+ return if @stack.size < 2
+
+ previous_workspace, current_workspace = @stack.pop(2)
+ @stack.push(current_workspace, previous_workspace)
+ end
+
+ # Replaces the current workspace with +workspace+.
+ def replace(workspace)
+ workspace.load_helper_methods_to_main
+ @stack[-1] = workspace
+ end
+
+ # Makes +workspace+ current while the block runs.
+ def with(workspace)
+ previous_workspace = current
+ replace(workspace)
+ yield
+ ensure
+ @stack[-1] = previous_workspace
+ end
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{@stack.map(&:main).inspect}>"
+ end
+ end
+end
diff --git a/test/irb/test_command.rb b/test/irb/test_command.rb
index 1a99bc7eb..f937cdecc 100644
--- a/test/irb/test_command.rb
+++ b/test/irb/test_command.rb
@@ -653,6 +653,17 @@ def test_chws_does_nothing_when_receiving_no_argument
assert_empty err
assert_include(out, "Current workspace: #{self}")
end
+
+ def test_chws_without_argument_returns_to_the_home_workspace_after_pushws
+ out, err = execute_lines(
+ "pushws #{self.class}::Foo.new\n",
+ "chws\n",
+ "self.class\n"
+ )
+ assert_empty err
+ assert_include(out, "Current workspace: #{self}")
+ assert_match(/=> #{self.class}\n$/, out)
+ end
end
class WhereamiTest < CommandTestCase
diff --git a/test/irb/test_workspace_stack.rb b/test/irb/test_workspace_stack.rb
new file mode 100644
index 000000000..4eccb3065
--- /dev/null
+++ b/test/irb/test_workspace_stack.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: false
+require 'irb'
+
+require_relative "helper"
+
+module TestIRB
+ class WorkspaceStackTest < TestCase
+ def setup
+ @home = IRB::WorkSpace.new(Object.new)
+ @stack = IRB::WorkspaceStack.new(@home)
+ end
+
+ def test_home_is_current_on_creation
+ assert_same(@home, @stack.home)
+ assert_same(@home, @stack.current)
+ assert_equal(1, @stack.size)
+ end
+
+ def test_push_and_pop
+ workspace = IRB::WorkSpace.new(Object.new)
+ @stack.push(workspace)
+ assert_same(workspace, @stack.current)
+ assert_same(@home, @stack.home)
+
+ assert_same(workspace, @stack.pop)
+ assert_same(@home, @stack.current)
+ end
+
+ def test_pop_never_removes_the_home_workspace
+ assert_nil(@stack.pop)
+ assert_same(@home, @stack.current)
+ end
+
+ def test_swap
+ workspace = IRB::WorkSpace.new(Object.new)
+ @stack.push(workspace)
+ @stack.swap
+ assert_equal([workspace, @home], @stack.to_a)
+ end
+
+ def test_replace_keeps_home_when_it_is_not_current
+ pushed = IRB::WorkSpace.new(Object.new)
+ replacement = IRB::WorkSpace.new(Object.new)
+ @stack.push(pushed)
+ @stack.replace(replacement)
+ assert_equal([@home, replacement], @stack.to_a)
+ end
+
+ def test_with_restores_the_current_workspace
+ workspace = IRB::WorkSpace.new(Object.new)
+ @stack.with(workspace) do
+ assert_same(workspace, @stack.current)
+ raise "error"
+ end
+ rescue RuntimeError
+ assert_same(@home, @stack.current)
+ end
+
+ def test_workspaces_get_helper_methods_when_they_become_current
+ main = Object.new
+ @stack.push(IRB::WorkSpace.new(main))
+ assert_include(main.singleton_class.ancestors, IRB::ExtendCommandBundle)
+
+ main = Object.new
+ @stack.replace(IRB::WorkSpace.new(main))
+ assert_include(main.singleton_class.ancestors, IRB::ExtendCommandBundle)
+ end
+ end
+end