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
46 changes: 46 additions & 0 deletions lib/irb/command/reload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module IRB
# :stopdoc:

module Command
class Reload < Base
category "IRB"
description "Reload files that were loaded via require in IRB session."

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

files = IRB.conf[:__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)
$LOADED_FEATURES.delete(path)
load path
$LOADED_FEATURES << 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(:reload, Command::Reload)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command name reload might be confused with Rails console's reload!. Consider alternatives like reload_requires or refresh.

end

ExtendCommand = Command
Expand Down
6 changes: 6 additions & 0 deletions lib/irb/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#

require 'set'

module IRB # :nodoc:
@CONF = {}
@INITIALIZED = false
Expand Down Expand Up @@ -196,6 +198,10 @@ def IRB.init_config(ap_path)
}

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

@CONF[:RELOADABLE_REQUIRE] = false
@CONF[:__RELOADABLE_FILES__] = Set.new
@CONF[:__AUTOLOAD_FILES__] = Set.new
end

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

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

module IRB
# Provides reload-aware require functionality for IRB.
#
# Limitations:
# - Native extensions cannot be reloaded (load doesn't support them)
# - Files loaded via box.require are not tracked
# - Constant redefinition warnings will appear on reload (uses load internally)
# - Context mode 5 (running IRB inside a Ruby::Box) is not supported
#
# This feature requires Ruby::Box (Ruby 4.0+).

class << self
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) }

IRB.conf[:__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
end

Ruby::Box.class_eval do
alias_method :__irb_original_require__, :require
alias_method :__irb_original_require_relative__, :require_relative

def __irb_reloadable_require__(feature)
unless IRB.conf[:__AUTOLOAD_FILES__].include?(feature)
return __irb_original_require__(feature)
end

IRB.conf[:__AUTOLOAD_FILES__].delete(feature)
IRB.track_and_load_files($LOADED_FEATURES, Ruby::Box.main) { __irb_original_require__(feature) }
end

def __irb_reloadable_require_relative__(feature)
__irb_original_require_relative__(feature)
end
end

module ReloadableRequire
class << self
def extended(base)
apply_autoload_hook
end

def apply_autoload_hook
Ruby::Box.class_eval do
alias_method :require, :__irb_reloadable_require__
alias_method :require_relative, :__irb_reloadable_require_relative__
end
end
end

private

def reloadable_require_internal(absolute_path, caller_box)
return false if caller_box.eval('$LOADED_FEATURES').include?(absolute_path)

box = Ruby::Box.new
load_path = caller_box.eval('$LOAD_PATH')
# Copy $LOAD_PATH to the box so it can resolve dependencies.
box.eval("$LOAD_PATH.concat(#{load_path})")

IRB.track_and_load_files(box.eval('$LOADED_FEATURES'), caller_box) { box.__irb_original_require__(absolute_path) }
end

def require(feature)
caller_loc = caller_locations(1, 1).first
current_box = Ruby::Box.main
resolved = current_box.eval("$LOAD_PATH.resolve_feature_path(#{feature.dump})")

# Fallback for calls outside IRB prompt
if caller_loc.path != "(irb)" || !resolved || resolved[0] != :rb
return current_box.require(feature)
end

reloadable_require_internal(resolved[1], current_box)
end

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

# Fallback for calls outside IRB prompt
if caller_loc.path != "(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

absolute_path = resolve_require_relative_path(feature)

if !absolute_path.end_with?('.rb') || !File.exist?(absolute_path)
file_path = File.join(Dir.pwd, "(irb)")
return current_box.eval("eval('Kernel.require_relative(#{feature.dump})', nil, #{file_path.dump}, 1)")
end

reloadable_require_internal(absolute_path, current_box)
end

def resolve_require_relative_path(feature)
absolute_path = File.expand_path(feature, Dir.pwd)
return absolute_path unless File.extname(absolute_path).empty?

dlext = RbConfig::CONFIG['DLEXT']
['.rb', ".#{dlext}"].each do |ext|
candidate = absolute_path + ext
return candidate if File.exist?(candidate)
end

absolute_path
end

def autoload(const, feature)
IRB.conf[:__AUTOLOAD_FILES__] << 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