Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions lib/irb/command/reload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module IRB
# :stopdoc:

module Command
class BoxReload < Base
category "IRB"
description "[Experimental] Reload files that were loaded via require in IRB session (requires Ruby::Box)."

help_message <<~HELP
Usage: box_reload

Reloads all Ruby files that were loaded via `require` or `require_relative`
during the current IRB session. This allows you to pick up changes made to
source files without restarting IRB.

Setup:
1. Start Ruby with RUBY_BOX=1 environment variable
2. Set IRB.conf[:RELOADABLE_REQUIRE] = true in your .irbrc

Example:
# In .irbrc:
IRB.conf[:RELOADABLE_REQUIRE] = true

# In IRB session:
require 'my_lib' # loaded and tracked
# ... edit my_lib.rb ...
box_reload # reloads the file

Note: This feature is experimental and requires Ruby::Box (Ruby 4.0+).
Native extensions (.so/.bundle) cannot be reloaded.
HELP

def execute(_arg)
unless reloadable_require_available?
warn "box_reload requires IRB.conf[:RELOADABLE_REQUIRE] = true and Ruby::Box (Ruby 4.0+) with RUBY_BOX=1 environment variable."
return
end

ReloadableRequire.collect_autoloaded_files
files = ReloadableRequire.reloadable_files
if files.empty?
puts "No files to reload. Use require to load files first."
return
end

files.each { |path| reload_file(path) }
end

private

def reloadable_require_available?
IRB.conf[:RELOADABLE_REQUIRE] && defined?(Ruby::Box) && Ruby::Box.enabled?
end

def reload_file(path)
load path
puts "Reloaded: #{path}"
rescue LoadError => e
warn "Failed to reload #{path}: #{e.message}"
rescue SyntaxError => e
warn "Syntax error in #{path}: #{e.message}"
end
end
end

# :startdoc:
end
2 changes: 2 additions & 0 deletions lib/irb/default_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
require_relative "command/measure"
require_relative "command/next"
require_relative "command/pushws"
require_relative "command/reload"
require_relative "command/show_doc"
require_relative "command/show_source"
require_relative "command/step"
Expand Down Expand Up @@ -252,6 +253,7 @@ def load_command(command)

register(:cd, Command::CD)
register(:copy, Command::Copy)
register(:box_reload, Command::BoxReload)
end

ExtendCommand = Command
Expand Down
2 changes: 2 additions & 0 deletions lib/irb/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ def IRB.init_config(ap_path)
}

@CONF[:COPY_COMMAND] = ENV.fetch("IRB_COPY_COMMAND", nil)

@CONF[:RELOADABLE_REQUIRE] = false
end

def IRB.set_measure_callback(type = nil, arg = nil, &block)
Expand Down
93 changes: 93 additions & 0 deletions lib/irb/reloadable_require.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

if !defined?(Ruby::Box) || !Ruby::Box.enabled?
raise "ReloadableRequire requires Ruby::Box to be enabled"
end

require 'set'

module IRB
# Provides reload-aware require functionality for IRB.
#
# This feature is experimental and requires Ruby::Box (Ruby 4.0+).
#
# Limitations:
# - Native extensions cannot be reloaded (load doesn't support them)
# - Constant redefinition warnings will appear on reload (uses load internally)

module ReloadableRequire
@reloadable_files = Set.new
@autoload_features = {}

class << self
attr_reader :reloadable_files, :autoload_features

def track_and_load_files(source, current_box)
before = source.dup
result = yield
new_files = source - before

return result if new_files.empty?

ruby_files, native_extensions = new_files.partition { |path| path.end_with?('.rb') }

native_extensions.each { |path| current_box.require(path) }

@reloadable_files.merge(ruby_files)

main_loaded_features = current_box.eval('$LOADED_FEATURES')
main_loaded_features.concat(ruby_files - main_loaded_features)
ruby_files.each { |path| current_box.load(path) }

result
end

def collect_autoloaded_files
@autoload_features.each_value do |feature|
resolved = $LOAD_PATH.resolve_feature_path(feature) rescue nil
next unless resolved && resolved.first == :rb
path = resolved[1]
@reloadable_files << path if $LOADED_FEATURES.include?(path)
end
end
end

private

def reloadable_require_internal(feature, caller_box)
box = Ruby::Box.new
box.eval("$LOAD_PATH.concat(#{caller_box.eval('$LOAD_PATH')})")
box.eval("$LOADED_FEATURES.concat(#{caller_box.eval('$LOADED_FEATURES')})")

ReloadableRequire.track_and_load_files(box.eval('$LOADED_FEATURES'), caller_box) { box.require(feature) }
end

def require(feature)
caller_loc = caller_locations(1, 1).first
return super unless caller_loc.path.end_with?("(irb)")

reloadable_require_internal(feature, Ruby::Box.main)
rescue LoadError
super
end

def require_relative(feature)
caller_loc = caller_locations(1, 1).first
current_box = Ruby::Box.main

unless caller_loc.path.end_with?("(irb)")
file_path = caller_loc.absolute_path || caller_loc.path
return current_box.eval("eval('Kernel.require_relative(#{feature.dump})', nil, #{file_path.dump}, #{caller_loc.lineno})")
end

reloadable_require_internal(File.expand_path(feature, Dir.pwd), current_box)
rescue LoadError
super
end

def autoload(const, feature)
ReloadableRequire.autoload_features[const.to_s] = feature
Ruby::Box.main.eval("Kernel.autoload(:#{const}, #{feature.dump})")
end
end
end
5 changes: 5 additions & 0 deletions lib/irb/workspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#

require_relative "helper_method"
require_relative "reloadable_require" if defined?(Ruby::Box) && Ruby::Box.enabled?

IRB::TOPLEVEL_BINDING = binding
module IRB # :nodoc:
Expand Down Expand Up @@ -103,6 +104,10 @@ def load_helper_methods_to_main
ancestors = class<<main;ancestors;end
main.extend ExtendCommandBundle if !ancestors.include?(ExtendCommandBundle)
main.extend HelpersContainer if !ancestors.include?(HelpersContainer)

if IRB.conf[:RELOADABLE_REQUIRE] && defined?(ReloadableRequire) && !ancestors.include?(ReloadableRequire)
main.extend ReloadableRequire
end
end

# Evaluate the given +statements+ within the context of this workspace.
Expand Down
Loading
Loading