From 34d6d751da8d43381e5be062e031b3b044700a7c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 10:47:05 +0000 Subject: [PATCH 1/5] Experiment: decouple workspace stack and Irb from Context Prototype for untangling Irb, Context and WorkSpace: - Add WorkspaceStack, which owns the workspace stack and its invariants (fixed home workspace, never empty, helper methods loaded whenever a workspace becomes current). Context delegates to it, and the methods that ext/workspaces.rb and ext/change-ws.rb used to monkey patch into Context now live in Context itself. - Fix `chws` with no argument not returning home after `pushws`, caused by home_workspace being memoized lazily from the current workspace. - Stop reading Context's ivars from the outside (pushws, Irb's suspend_input_method). - Move from_binding onto Context and statement dispatch onto Irb, which passes itself to commands. Internal code no longer reaches Irb via Context#irb (only multi-irb still does). - Extract WorkSpace.base_binding from WorkSpace#initialize. - Remove the dead Context#workspace_home reader and IrbLoader#old. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175QjMQGeUPByLSqoL6ye5U --- lib/irb.rb | 43 ++++++++++------ lib/irb/command/base.rb | 15 +++++- lib/irb/command/chws.rb | 1 - lib/irb/command/debug.rb | 2 +- lib/irb/command/pushws.rb | 4 +- lib/irb/context.rb | 87 ++++++++++++++++++++------------ lib/irb/ext/change-ws.rb | 37 +------------- lib/irb/ext/loader.rb | 30 ----------- lib/irb/ext/workspaces.rb | 36 +------------ lib/irb/workspace.rb | 48 ++++++++++-------- lib/irb/workspace_stack.rb | 77 ++++++++++++++++++++++++++++ test/irb/test_command.rb | 11 ++++ test/irb/test_workspace_stack.rb | 69 +++++++++++++++++++++++++ 13 files changed, 283 insertions(+), 177 deletions(-) create mode 100644 lib/irb/workspace_stack.rb create mode 100644 test/irb/test_workspace_stack.rb diff --git a/lib/irb.rb b/lib/irb.rb index 86a1b6f6b..cfd3797e1 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: @@ -220,7 +219,7 @@ def eval_input return statement.code end - @context.evaluate(statement, line_no) + evaluate_statement(statement, line_no) if @context.echo? && !statement.suppresses_echo? if statement.is_assignment? @@ -241,6 +240,22 @@ def eval_input end end + def evaluate_statement(statement, line_no) # :nodoc: + case statement + when Statement::EmptyInput + return + when Statement::Expression + result = @context.evaluate_expression(statement.code, line_no) + @context.set_last_value(result) + when Statement::Command + statement.command_class.execute(@context, statement.arg, irb: self) + when Statement::IncorrectAlias + warn statement.message + end + + nil + end + def read_input(prompt) signal_status(:IN_INPUT) do @context.io.prompt = prompt @@ -489,12 +504,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 +514,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/base.rb b/lib/irb/command/base.rb index 720e47af8..2f44b5fe9 100644 --- a/lib/irb/command/base.rb +++ b/lib/irb/command/base.rb @@ -31,8 +31,12 @@ def help_message(help_message = nil) @help_message end - def execute(irb_context, arg) - new(irb_context).execute(arg) + def execute(irb_context, arg, irb: nil) + # The Irb is set after construction so subclasses that override + # #initialize keep working. + command = new(irb_context) + command.irb = irb + command.execute(arg) rescue CommandArgumentError => e puts e.message end @@ -84,6 +88,13 @@ def initialize(irb_context) end attr_reader :irb_context + attr_writer :irb # :nodoc: + + # The Irb running this command. Falls back to Context#irb for commands + # executed outside of Irb's loop. + def irb + @irb || irb_context.irb + end def execute(arg) #nop 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/debug.rb b/lib/irb/command/debug.rb index 3ebb57fe5..69e64f733 100644 --- a/lib/irb/command/debug.rb +++ b/lib/irb/command/debug.rb @@ -41,7 +41,7 @@ def execute_debug_command(pre_cmds: nil, do_cmds: nil) return end - unless IRB::Debug.setup(irb_context.irb) + unless IRB::Debug.setup(irb) puts <<~MSG You need to install the debug gem before using this command. If you use `bundle exec`, please add `gem "debug"` into your Gemfile. 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..452fee8b4 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. @@ -217,7 +251,10 @@ def main # context is created. See ::new for more # information on +input_method+. attr_accessor :io - # Current irb session. + # The Irb that owns this context. + # + # Prefer not to depend on this: commands get the Irb running them through + # Command::Base#irb. attr_accessor :irb # A copy of the default IRB.conf[:AP_NAME] attr_accessor :ap_name @@ -547,29 +584,13 @@ def inspect_mode=(opt) @inspect_mode end - def evaluate(statement, line_no) # :nodoc: - @line_no = line_no - - case statement - when Statement::EmptyInput - return - when Statement::Expression - result = evaluate_expression(statement.code, line_no) - set_last_value(result) - when Statement::Command - statement.command_class.execute(self, statement.arg) - when Statement::IncorrectAlias - warn statement.message - end - - 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: + @line_no = line_no result = nil if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty? IRB.set_measure_callback diff --git a/lib/irb/ext/change-ws.rb b/lib/irb/ext/change-ws.rb index 60e8afe31..89e601499 100644 --- a/lib/irb/ext/change-ws.rb +++ b/lib/irb/ext/change-ws.rb @@ -1,37 +1,4 @@ # 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 +# The workspace methods that used to be defined here now live in +# irb/context.rb. This file is kept so existing requires keep working. diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb index 0955b4acf..52a31f286 100644 --- a/lib/irb/ext/loader.rb +++ b/lib/irb/ext/loader.rb @@ -39,7 +39,6 @@ def search_file_from_ruby_path(fn) # :nodoc: # # See Irb#suspend_input_method for more information. def source_file(path) - irb = irb_context.irb irb.suspend_name(path, File.basename(path)) do FileInputMethod.open(path) do |io| irb.suspend_input_method(io) do @@ -64,7 +63,6 @@ def source_file(path) # # See Irb#suspend_input_method for more information. def load_file(path, priv = nil) - irb = irb_context.irb irb.suspend_name(path, File.basename(path)) do if priv @@ -92,33 +90,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 index da09faa83..89e601499 100644 --- a/lib/irb/ext/workspaces.rb +++ b/lib/irb/ext/workspaces.rb @@ -1,36 +1,4 @@ # 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 +# The workspace methods that used to be defined here now live in +# irb/context.rb. This file is kept so existing requires keep working. 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 From 52b16cc6132b75e75a3e1bc33e0dee9c0c101154 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 12:33:54 +0000 Subject: [PATCH 2/5] Inject the Irb into commands through the constructor Measure's initialize override was a no-op, and it was the only thing that forced the setter. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175QjMQGeUPByLSqoL6ye5U --- lib/irb/command/base.rb | 10 +++------- lib/irb/command/measure.rb | 4 ---- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb index 2f44b5fe9..4e2d73424 100644 --- a/lib/irb/command/base.rb +++ b/lib/irb/command/base.rb @@ -32,11 +32,7 @@ def help_message(help_message = nil) end def execute(irb_context, arg, irb: nil) - # The Irb is set after construction so subclasses that override - # #initialize keep working. - command = new(irb_context) - command.irb = irb - command.execute(arg) + new(irb_context, irb: irb).execute(arg) rescue CommandArgumentError => e puts e.message end @@ -83,12 +79,12 @@ def wrap_lines(text, width) end end - def initialize(irb_context) + def initialize(irb_context, irb: nil) @irb_context = irb_context + @irb = irb end attr_reader :irb_context - attr_writer :irb # :nodoc: # The Irb running this command. Falls back to Context#irb for commands # executed outside of Irb's loop. 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.' From eb86113c9afa63142aeffe370826939fd954555d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 12:37:25 +0000 Subject: [PATCH 3/5] Remove the emptied ext/workspaces.rb and ext/change-ws.rb Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175QjMQGeUPByLSqoL6ye5U --- lib/irb/ext/change-ws.rb | 4 ---- lib/irb/ext/workspaces.rb | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 lib/irb/ext/change-ws.rb delete mode 100644 lib/irb/ext/workspaces.rb diff --git a/lib/irb/ext/change-ws.rb b/lib/irb/ext/change-ws.rb deleted file mode 100644 index 89e601499..000000000 --- a/lib/irb/ext/change-ws.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true -# -# The workspace methods that used to be defined here now live in -# irb/context.rb. This file is kept so existing requires keep working. diff --git a/lib/irb/ext/workspaces.rb b/lib/irb/ext/workspaces.rb deleted file mode 100644 index 89e601499..000000000 --- a/lib/irb/ext/workspaces.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true -# -# The workspace methods that used to be defined here now live in -# irb/context.rb. This file is kept so existing requires keep working. From ddda23f3538535e67de15781bcb41aab787b29a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 13:59:07 +0000 Subject: [PATCH 4/5] Reach the Irb through Context#irb in commands again Context#irb is always the Irb running the command, so injecting it separately added an API without changing behavior. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175QjMQGeUPByLSqoL6ye5U --- lib/irb.rb | 2 +- lib/irb/command/base.rb | 13 +++---------- lib/irb/command/debug.rb | 2 +- lib/irb/context.rb | 5 +---- lib/irb/ext/loader.rb | 2 ++ 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/irb.rb b/lib/irb.rb index cfd3797e1..90ba94fdd 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -248,7 +248,7 @@ def evaluate_statement(statement, line_no) # :nodoc: result = @context.evaluate_expression(statement.code, line_no) @context.set_last_value(result) when Statement::Command - statement.command_class.execute(@context, statement.arg, irb: self) + statement.command_class.execute(@context, statement.arg) when Statement::IncorrectAlias warn statement.message end diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb index 4e2d73424..720e47af8 100644 --- a/lib/irb/command/base.rb +++ b/lib/irb/command/base.rb @@ -31,8 +31,8 @@ def help_message(help_message = nil) @help_message end - def execute(irb_context, arg, irb: nil) - new(irb_context, irb: irb).execute(arg) + def execute(irb_context, arg) + new(irb_context).execute(arg) rescue CommandArgumentError => e puts e.message end @@ -79,19 +79,12 @@ def wrap_lines(text, width) end end - def initialize(irb_context, irb: nil) + def initialize(irb_context) @irb_context = irb_context - @irb = irb end attr_reader :irb_context - # The Irb running this command. Falls back to Context#irb for commands - # executed outside of Irb's loop. - def irb - @irb || irb_context.irb - end - def execute(arg) #nop end diff --git a/lib/irb/command/debug.rb b/lib/irb/command/debug.rb index 69e64f733..3ebb57fe5 100644 --- a/lib/irb/command/debug.rb +++ b/lib/irb/command/debug.rb @@ -41,7 +41,7 @@ def execute_debug_command(pre_cmds: nil, do_cmds: nil) return end - unless IRB::Debug.setup(irb) + unless IRB::Debug.setup(irb_context.irb) puts <<~MSG You need to install the debug gem before using this command. If you use `bundle exec`, please add `gem "debug"` into your Gemfile. diff --git a/lib/irb/context.rb b/lib/irb/context.rb index 452fee8b4..cc1c2e694 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -251,10 +251,7 @@ def main # context is created. See ::new for more # information on +input_method+. attr_accessor :io - # The Irb that owns this context. - # - # Prefer not to depend on this: commands get the Irb running them through - # Command::Base#irb. + # Current irb session. attr_accessor :irb # A copy of the default IRB.conf[:AP_NAME] attr_accessor :ap_name diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb index 52a31f286..a7f0cadc0 100644 --- a/lib/irb/ext/loader.rb +++ b/lib/irb/ext/loader.rb @@ -39,6 +39,7 @@ def search_file_from_ruby_path(fn) # :nodoc: # # See Irb#suspend_input_method for more information. def source_file(path) + irb = irb_context.irb irb.suspend_name(path, File.basename(path)) do FileInputMethod.open(path) do |io| irb.suspend_input_method(io) do @@ -63,6 +64,7 @@ def source_file(path) # # See Irb#suspend_input_method for more information. def load_file(path, priv = nil) + irb = irb_context.irb irb.suspend_name(path, File.basename(path)) do if priv From f3a4c9c3ab11d1b8e8ddb5c5f64194848b1a2189 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 11:59:04 +0000 Subject: [PATCH 5/5] Keep statement dispatch in Context#evaluate debug.gem prepends a patch to IRB::Context#evaluate to restart its threads around each evaluation, so moving dispatch to Irb broke the debugger's IRB console. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175QjMQGeUPByLSqoL6ye5U --- lib/irb.rb | 18 +----------------- lib/irb/context.rb | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/irb.rb b/lib/irb.rb index 90ba94fdd..8857c34e6 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -219,7 +219,7 @@ def eval_input return statement.code end - evaluate_statement(statement, line_no) + @context.evaluate(statement, line_no) if @context.echo? && !statement.suppresses_echo? if statement.is_assignment? @@ -240,22 +240,6 @@ def eval_input end end - def evaluate_statement(statement, line_no) # :nodoc: - case statement - when Statement::EmptyInput - return - when Statement::Expression - result = @context.evaluate_expression(statement.code, line_no) - @context.set_last_value(result) - when Statement::Command - statement.command_class.execute(@context, statement.arg) - when Statement::IncorrectAlias - warn statement.message - end - - nil - end - def read_input(prompt) signal_status(:IN_INPUT) do @context.io.prompt = prompt diff --git a/lib/irb/context.rb b/lib/irb/context.rb index cc1c2e694..02db88772 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -581,13 +581,30 @@ def inspect_mode=(opt) @inspect_mode end + def evaluate(statement, line_no) # :nodoc: + @line_no = line_no + + case statement + when Statement::EmptyInput + return + when Statement::Expression + result = evaluate_expression(statement.code, line_no) + set_last_value(result) + when Statement::Command + statement.command_class.execute(self, statement.arg) + when Statement::IncorrectAlias + warn statement.message + end + + nil + end + # Whether this context was started by Binding#irb. def from_binding? @from_binding end def evaluate_expression(code, line_no) # :nodoc: - @line_no = line_no result = nil if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty? IRB.set_measure_callback