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