summaryrefslogtreecommitdiff
path: root/lib/irb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/irb')
-rw-r--r--lib/irb/cmd/chws.rb36
-rw-r--r--lib/irb/cmd/fork.rb40
-rw-r--r--lib/irb/cmd/help.rb49
-rw-r--r--lib/irb/cmd/info.rb34
-rw-r--r--lib/irb/cmd/load.rb68
-rw-r--r--lib/irb/cmd/nop.rb49
-rw-r--r--lib/irb/cmd/pushws.rb42
-rw-r--r--lib/irb/cmd/show_source.rb95
-rw-r--r--lib/irb/cmd/subirb.rb45
-rw-r--r--lib/irb/color.rb70
-rw-r--r--lib/irb/color_printer.rb10
-rw-r--r--lib/irb/command.rb23
-rw-r--r--lib/irb/command/backtrace.rb17
-rw-r--r--lib/irb/command/base.rb62
-rw-r--r--lib/irb/command/break.rb17
-rw-r--r--lib/irb/command/catch.rb17
-rw-r--r--lib/irb/command/chws.rb40
-rw-r--r--lib/irb/command/context.rb16
-rw-r--r--lib/irb/command/continue.rb17
-rw-r--r--lib/irb/command/debug.rb86
-rw-r--r--lib/irb/command/delete.rb17
-rw-r--r--lib/irb/command/disable_irb.rb19
-rw-r--r--lib/irb/command/edit.rb63
-rw-r--r--lib/irb/command/exit.rb18
-rw-r--r--lib/irb/command/finish.rb17
-rw-r--r--lib/irb/command/force_exit.rb18
-rw-r--r--lib/irb/command/help.rb75
-rw-r--r--lib/irb/command/history.rb45
-rw-r--r--lib/irb/command/info.rb17
-rw-r--r--lib/irb/command/internal_helpers.rb27
-rw-r--r--lib/irb/command/irb_info.rb33
-rw-r--r--lib/irb/command/load.rb91
-rw-r--r--lib/irb/command/ls.rb (renamed from lib/irb/cmd/ls.rb)88
-rw-r--r--lib/irb/command/measure.rb (renamed from lib/irb/cmd/measure.rb)38
-rw-r--r--lib/irb/command/next.rb17
-rw-r--r--lib/irb/command/pushws.rb65
-rw-r--r--lib/irb/command/show_doc.rb51
-rw-r--r--lib/irb/command/show_source.rb74
-rw-r--r--lib/irb/command/step.rb17
-rw-r--r--lib/irb/command/subirb.rb123
-rw-r--r--lib/irb/command/whereami.rb (renamed from lib/irb/cmd/whereami.rb)11
-rw-r--r--lib/irb/completion.rb362
-rw-r--r--lib/irb/context.rb269
-rw-r--r--lib/irb/debug.rb130
-rw-r--r--lib/irb/debug/ui.rb103
-rw-r--r--lib/irb/default_commands.rb260
-rw-r--r--lib/irb/easter-egg.rb22
-rw-r--r--lib/irb/ext/change-ws.rb20
-rw-r--r--lib/irb/ext/eval_history.rb (renamed from lib/irb/ext/history.rb)20
-rw-r--r--lib/irb/ext/loader.rb42
-rw-r--r--lib/irb/ext/multi-irb.rb17
-rw-r--r--lib/irb/ext/save-history.rb130
-rw-r--r--lib/irb/ext/tracer.rb73
-rw-r--r--lib/irb/ext/use-loader.rb20
-rw-r--r--lib/irb/ext/workspaces.rb50
-rw-r--r--lib/irb/extend-command.rb356
-rw-r--r--lib/irb/frame.rb8
-rw-r--r--lib/irb/help.rb16
-rw-r--r--lib/irb/helper_method.rb29
-rw-r--r--lib/irb/helper_method/base.rb16
-rw-r--r--lib/irb/helper_method/conf.rb11
-rw-r--r--lib/irb/history.rb87
-rw-r--r--lib/irb/init.rb229
-rw-r--r--lib/irb/input-method.rb467
-rw-r--r--lib/irb/inspector.rb28
-rw-r--r--lib/irb/irb.gemspec14
-rw-r--r--lib/irb/lc/error.rb23
-rw-r--r--lib/irb/lc/help-message100
-rw-r--r--lib/irb/lc/ja/encoding_aliases.rb13
-rw-r--r--lib/irb/lc/ja/error.rb25
-rw-r--r--lib/irb/lc/ja/help-message25
-rw-r--r--lib/irb/locale.rb64
-rw-r--r--lib/irb/magic-file.rb38
-rw-r--r--lib/irb/nesting_parser.rb237
-rw-r--r--lib/irb/notifier.rb8
-rw-r--r--lib/irb/output-method.rb16
-rw-r--r--lib/irb/pager.rb91
-rw-r--r--lib/irb/ruby-lex.rb1141
-rw-r--r--lib/irb/ruby_logo.aa43
-rw-r--r--lib/irb/source_finder.rb139
-rw-r--r--lib/irb/src_encoding.rb7
-rw-r--r--lib/irb/statement.rb80
-rw-r--r--lib/irb/version.rb12
-rw-r--r--lib/irb/workspace.rb43
-rw-r--r--lib/irb/ws-for-case-2.rb8
-rw-r--r--lib/irb/xmp.rb12
86 files changed, 3864 insertions, 2797 deletions
diff --git a/lib/irb/cmd/chws.rb b/lib/irb/cmd/chws.rb
deleted file mode 100644
index b28c090686..0000000000
--- a/lib/irb/cmd/chws.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: false
-#
-# change-ws.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require_relative "nop"
-require_relative "../ext/change-ws"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
-
- class CurrentWorkingWorkspace < Nop
- def execute(*obj)
- irb_context.main
- end
- end
-
- class ChangeWorkspace < Nop
- def execute(*obj)
- irb_context.change_workspace(*obj)
- irb_context.main
- end
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/cmd/fork.rb b/lib/irb/cmd/fork.rb
deleted file mode 100644
index 255a670dce..0000000000
--- a/lib/irb/cmd/fork.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: false
-#
-# fork.rb -
-# $Release Version: 0.9.6 $
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require_relative "nop"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
- class Fork < Nop
- def execute
- pid = __send__ ExtendCommand.irb_original_method_name("fork")
- unless pid
- class << self
- alias_method :exit, ExtendCommand.irb_original_method_name('exit')
- end
- if block_given?
- begin
- yield
- ensure
- exit
- end
- end
- end
- pid
- end
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/cmd/help.rb b/lib/irb/cmd/help.rb
deleted file mode 100644
index 0497c57457..0000000000
--- a/lib/irb/cmd/help.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: false
-#
-# help.rb - helper using ri
-# $Release Version: 0.9.6$
-# $Revision$
-#
-# --
-#
-#
-#
-
-require_relative "nop"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
- class Help < Nop
- def execute(*names)
- require 'rdoc/ri/driver'
- opts = RDoc::RI::Driver.process_args([])
- IRB::ExtendCommand::Help.const_set(:Ri, RDoc::RI::Driver.new(opts))
- rescue LoadError, SystemExit
- IRB::ExtendCommand::Help.remove_method(:execute)
- # raise NoMethodError in ensure
- else
- def execute(*names)
- if names.empty?
- Ri.interactive
- return
- end
- names.each do |name|
- begin
- Ri.display_name(name.to_s)
- rescue RDoc::RI::Error
- puts $!.message
- end
- end
- nil
- end
- nil
- ensure
- execute(*names)
- end
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/cmd/info.rb b/lib/irb/cmd/info.rb
deleted file mode 100644
index 37ecd762ff..0000000000
--- a/lib/irb/cmd/info.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: false
-
-require_relative "nop"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
- class Info < Nop
- def execute
- Class.new {
- def inspect
- str = "Ruby version: #{RUBY_VERSION}\n"
- str += "IRB version: #{IRB.version}\n"
- str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n"
- str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file)
- str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n"
- str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty?
- str += "LC_ALL env: #{ENV["LC_ALL"]}\n" if ENV["LC_ALL"] && !ENV["LC_ALL"].empty?
- str += "East Asian Ambiguous Width: #{Reline.ambiguous_width.inspect}\n"
- if RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
- codepage = `chcp`.b.sub(/.*: (\d+)\n/, '\1')
- str += "Code page: #{codepage}\n"
- end
- str
- end
- alias_method :to_s, :inspect
- }.new
- end
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/cmd/load.rb b/lib/irb/cmd/load.rb
deleted file mode 100644
index 2c5c01e89c..0000000000
--- a/lib/irb/cmd/load.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: false
-#
-# load.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require_relative "nop"
-require_relative "../ext/loader"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
- class Load < Nop
- include IrbLoader
-
- def execute(file_name, priv = nil)
- return irb_load(file_name, priv)
- end
- end
-
- class Require < Nop
- include IrbLoader
-
- def execute(file_name)
-
- rex = Regexp.new("#{Regexp.quote(file_name)}(\.o|\.rb)?")
- return false if $".find{|f| f =~ rex}
-
- case file_name
- when /\.rb$/
- begin
- if irb_load(file_name)
- $".push file_name
- return true
- end
- rescue LoadError
- end
- when /\.(so|o|sl)$/
- return ruby_require(file_name)
- end
-
- begin
- irb_load(f = file_name + ".rb")
- $".push f
- return true
- rescue LoadError
- return ruby_require(file_name)
- end
- end
- end
-
- class Source < Nop
- include IrbLoader
- def execute(file_name)
- source_file(file_name)
- end
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/cmd/nop.rb b/lib/irb/cmd/nop.rb
index 881a736722..9d2e3c4d47 100644
--- a/lib/irb/cmd/nop.rb
+++ b/lib/irb/cmd/nop.rb
@@ -1,47 +1,4 @@
-# frozen_string_literal: false
-#
-# nop.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-module IRB
- # :stopdoc:
+# frozen_string_literal: true
- module ExtendCommand
- class Nop
-
- if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.7.0"
- def self.execute(conf, *opts, **kwargs, &block)
- command = new(conf)
- command.execute(*opts, **kwargs, &block)
- end
- else
- def self.execute(conf, *opts, &block)
- command = new(conf)
- command.execute(*opts, &block)
- end
- end
-
- def initialize(conf)
- @irb_context = conf
- end
-
- attr_reader :irb_context
-
- def irb
- @irb_context.irb
- end
-
- def execute(*opts)
- #nop
- end
- end
- end
-
- # :startdoc:
-end
+# This file is just a placeholder for backward-compatibility.
+# Please require 'irb' and inherit your command from `IRB::Command::Base` instead.
diff --git a/lib/irb/cmd/pushws.rb b/lib/irb/cmd/pushws.rb
deleted file mode 100644
index 791d8f8dbb..0000000000
--- a/lib/irb/cmd/pushws.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: false
-#
-# change-ws.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require_relative "nop"
-require_relative "../ext/workspaces"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
- class Workspaces < Nop
- def execute(*obj)
- irb_context.workspaces.collect{|ws| ws.main}
- end
- end
-
- class PushWorkspace < Workspaces
- def execute(*obj)
- irb_context.push_workspace(*obj)
- super
- end
- end
-
- class PopWorkspace < Workspaces
- def execute(*obj)
- irb_context.pop_workspace(*obj)
- super
- end
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb
deleted file mode 100644
index f8a17822df..0000000000
--- a/lib/irb/cmd/show_source.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "nop"
-require_relative "../color"
-require_relative "../ruby-lex"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
- class ShowSource < Nop
- def execute(str = nil)
- unless str.is_a?(String)
- puts "Error: Expected a string but got #{str.inspect}"
- return
- end
- source = find_source(str)
- if source && File.exist?(source.file)
- show_source(source)
- else
- puts "Error: Couldn't locate a definition for #{str}"
- end
- nil
- end
-
- private
-
- # @param [IRB::ExtendCommand::ShowSource::Source] source
- def show_source(source)
- puts
- puts "#{bold("From")}: #{source.file}:#{source.first_line}"
- puts
- code = IRB::Color.colorize_code(File.read(source.file))
- puts code.lines[(source.first_line - 1)...source.last_line].join
- puts
- end
-
- def find_source(str)
- case str
- when /\A[A-Z]\w*(::[A-Z]\w*)*\z/ # Const::Name
- eval(str, irb_context.workspace.binding) # trigger autoload
- base = irb_context.workspace.binding.receiver.yield_self { |r| r.is_a?(Module) ? r : Object }
- file, line = base.const_source_location(str) if base.respond_to?(:const_source_location) # Ruby 2.7+
- when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method
- owner = eval(Regexp.last_match[:owner], irb_context.workspace.binding)
- method = Regexp.last_match[:method]
- if owner.respond_to?(:instance_method) && owner.instance_methods.include?(method.to_sym)
- file, line = owner.instance_method(method).source_location
- end
- when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method
- receiver = eval(Regexp.last_match[:receiver] || 'self', irb_context.workspace.binding)
- method = Regexp.last_match[:method]
- file, line = receiver.method(method).source_location if receiver.respond_to?(method)
- end
- if file && line
- Source.new(file: file, first_line: line, last_line: find_end(file, line))
- end
- end
-
- def find_end(file, first_line)
- return first_line unless File.exist?(file)
- lex = RubyLex.new
- lines = File.read(file).lines[(first_line - 1)..-1]
- tokens = RubyLex.ripper_lex_without_warning(lines.join)
- prev_tokens = []
-
- # chunk with line number
- tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
- code = lines[0..lnum].join
- prev_tokens.concat chunk
- continue = lex.process_continue(prev_tokens)
- code_block_open = lex.check_code_block(code, prev_tokens)
- if !continue && !code_block_open
- return first_line + lnum
- end
- end
- first_line
- end
-
- def bold(str)
- Color.colorize(str, [:BOLD])
- end
-
- Source = Struct.new(
- :file, # @param [String] - file name
- :first_line, # @param [String] - first line
- :last_line, # @param [String] - last line
- keyword_init: true,
- )
- private_constant :Source
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/cmd/subirb.rb b/lib/irb/cmd/subirb.rb
deleted file mode 100644
index b322aadc53..0000000000
--- a/lib/irb/cmd/subirb.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: false
-# multi.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-require_relative "nop"
-require_relative "../ext/multi-irb"
-
-module IRB
- # :stopdoc:
-
- module ExtendCommand
- class IrbCommand < Nop
- def execute(*obj)
- IRB.irb(nil, *obj)
- end
- end
-
- class Jobs < Nop
- def execute
- IRB.JobManager
- end
- end
-
- class Foreground < Nop
- def execute(key)
- IRB.JobManager.switch(key)
- end
- end
-
- class Kill < Nop
- def execute(*keys)
- IRB.JobManager.kill(*keys)
- end
- end
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/color.rb b/lib/irb/color.rb
index 8307af25a9..ad8670160c 100644
--- a/lib/irb/color.rb
+++ b/lib/irb/color.rb
@@ -9,12 +9,14 @@ module IRB # :nodoc:
BOLD = 1
UNDERLINE = 4
REVERSE = 7
+ BLACK = 30
RED = 31
GREEN = 32
YELLOW = 33
BLUE = 34
MAGENTA = 35
CYAN = 36
+ WHITE = 37
TOKEN_KEYWORDS = {
on_kw: ['nil', 'self', 'true', 'false', '__FILE__', '__LINE__', '__ENCODING__'],
@@ -123,15 +125,21 @@ module IRB # :nodoc:
# If `complete` is false (code is incomplete), this does not warn compile_error.
# This option is needed to avoid warning a user when the compile_error is happening
# because the input is not wrong but just incomplete.
- def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?)
+ def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?, local_variables: [])
return code unless colorable
symbol_state = SymbolState.new
colored = +''
- length = 0
- end_seen = false
+ lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
+ code_with_lvars = lvars_code ? "#{lvars_code}\n#{code}" : code
+
+ scan(code_with_lvars, allow_last_error: !complete) do |token, str, expr|
+ # handle uncolorable code
+ if token.nil?
+ colored << Reline::Unicode.escape_for_print(str)
+ next
+ end
- scan(code, allow_last_error: !complete) do |token, str, expr|
# IRB::ColorPrinter skips colorizing fragments with any invalid token
if ignore_error && ERROR_TOKENS.include?(token)
return Reline::Unicode.escape_for_print(code)
@@ -147,15 +155,12 @@ module IRB # :nodoc:
colored << line
end
end
- length += str.bytesize
- end_seen = true if token == :on___end__
end
- # give up colorizing incomplete Ripper tokens
- unless end_seen or length == code.bytesize
- return Reline::Unicode.escape_for_print(code)
+ if lvars_code
+ raise "#{lvars_code.dump} should have no \\n" if lvars_code.include?("\n")
+ colored.sub!(/\A.+\n/, '') # delete_prefix lvars_code with colors
end
-
colored
end
@@ -170,33 +175,36 @@ module IRB # :nodoc:
end
def scan(code, allow_last_error:)
- pos = [1, 0]
-
verbose, $VERBOSE = $VERBOSE, nil
RubyLex.compile_with_errors_suppressed(code) do |inner_code, line_no|
lexer = Ripper::Lexer.new(inner_code, '(ripper)', line_no)
- if lexer.respond_to?(:scan) # Ruby 2.7+
- lexer.scan.each do |elem|
- str = elem.tok
- next if allow_last_error and /meets end of file|unexpected end-of-input/ =~ elem.message
- next if ([elem.pos[0], elem.pos[1] + str.bytesize] <=> pos) <= 0
-
- str.each_line do |line|
- if line.end_with?("\n")
- pos[0] += 1
- pos[1] = 0
- else
- pos[1] += line.bytesize
- end
- end
+ byte_pos = 0
+ line_positions = [0]
+ inner_code.lines.each do |line|
+ line_positions << line_positions.last + line.bytesize
+ end
- yield(elem.event, str, elem.state)
+ on_scan = proc do |elem|
+ start_pos = line_positions[elem.pos[0] - 1] + elem.pos[1]
+
+ # yield uncolorable code
+ if byte_pos < start_pos
+ yield(nil, inner_code.byteslice(byte_pos...start_pos), nil)
end
- else
- lexer.parse.each do |elem|
- yield(elem.event, elem.tok, elem.state)
+
+ if byte_pos <= start_pos
+ str = elem.tok
+ yield(elem.event, str, elem.state)
+ byte_pos = start_pos + str.bytesize
end
end
+
+ lexer.scan.each do |elem|
+ next if allow_last_error and /meets end of file|unexpected end-of-input/ =~ elem.message
+ on_scan.call(elem)
+ end
+ # yield uncolorable DATA section
+ yield(nil, inner_code.byteslice(byte_pos...inner_code.bytesize), nil) if byte_pos < inner_code.bytesize
end
ensure
$VERBOSE = verbose
@@ -230,7 +238,7 @@ module IRB # :nodoc:
case token
when :on_symbeg, :on_symbols_beg, :on_qsymbols_beg
@stack << true
- when :on_ident, :on_op, :on_const, :on_ivar, :on_cvar, :on_gvar, :on_kw
+ when :on_ident, :on_op, :on_const, :on_ivar, :on_cvar, :on_gvar, :on_kw, :on_backtick
if @stack.last # Pop only when it's Symbol
@stack.pop
return prev_state
diff --git a/lib/irb/color_printer.rb b/lib/irb/color_printer.rb
index 78f0b51520..31644aa7f9 100644
--- a/lib/irb/color_printer.rb
+++ b/lib/irb/color_printer.rb
@@ -4,6 +4,9 @@ require_relative 'color'
module IRB
class ColorPrinter < ::PP
+ METHOD_RESPOND_TO = Object.instance_method(:respond_to?)
+ METHOD_INSPECT = Object.instance_method(:inspect)
+
class << self
def pp(obj, out = $>, width = screen_width)
q = ColorPrinter.new(out, width)
@@ -22,9 +25,11 @@ module IRB
end
def pp(obj)
- if obj.is_a?(String)
+ if String === obj
# Avoid calling Ruby 2.4+ String#pretty_print that splits a string by "\n"
text(obj.inspect)
+ elsif !METHOD_RESPOND_TO.bind(obj).call(:inspect)
+ text(METHOD_INSPECT.bind(obj).call)
else
super
end
@@ -37,6 +42,9 @@ module IRB
width ||= str.length
case str
+ when ''
+ when ',', '=>', '[', ']', '{', '}', '..', '...', /\A@\w+\z/
+ super(str, width)
when /\A#</, '=', '>'
super(Color.colorize(str, [:GREEN]), width)
else
diff --git a/lib/irb/command.rb b/lib/irb/command.rb
new file mode 100644
index 0000000000..68a4b52727
--- /dev/null
+++ b/lib/irb/command.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+#
+# irb/command.rb - irb command
+# by Keiju ISHITSUKA(keiju@ruby-lang.org)
+#
+
+require_relative "command/base"
+
+module IRB # :nodoc:
+ module Command
+ @commands = {}
+
+ class << self
+ attr_reader :commands
+
+ # Registers a command with the given name.
+ # Aliasing is intentionally not supported at the moment.
+ def register(name, command_class)
+ @commands[name.to_sym] = [command_class, []]
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/backtrace.rb b/lib/irb/command/backtrace.rb
new file mode 100644
index 0000000000..687bb075ac
--- /dev/null
+++ b/lib/irb/command/backtrace.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Backtrace < DebugCommand
+ def execute(arg)
+ execute_debug_command(pre_cmds: "backtrace #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb
new file mode 100644
index 0000000000..b078b48237
--- /dev/null
+++ b/lib/irb/command/base.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+#
+# nop.rb -
+# by Keiju ISHITSUKA(keiju@ruby-lang.org)
+#
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class CommandArgumentError < StandardError; end
+
+ def self.extract_ruby_args(*args, **kwargs)
+ throw :EXTRACT_RUBY_ARGS, [args, kwargs]
+ end
+
+ class Base
+ class << self
+ def category(category = nil)
+ @category = category if category
+ @category
+ end
+
+ def description(description = nil)
+ @description = description if description
+ @description
+ end
+
+ def help_message(help_message = nil)
+ @help_message = help_message if help_message
+ @help_message
+ end
+
+ private
+
+ def highlight(text)
+ Color.colorize(text, [:BOLD, :BLUE])
+ end
+ end
+
+ def self.execute(irb_context, arg)
+ new(irb_context).execute(arg)
+ rescue CommandArgumentError => e
+ puts e.message
+ end
+
+ def initialize(irb_context)
+ @irb_context = irb_context
+ end
+
+ attr_reader :irb_context
+
+ def execute(arg)
+ #nop
+ end
+ end
+
+ Nop = Base
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/break.rb b/lib/irb/command/break.rb
new file mode 100644
index 0000000000..a8f81fe665
--- /dev/null
+++ b/lib/irb/command/break.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Break < DebugCommand
+ def execute(arg)
+ execute_debug_command(pre_cmds: "break #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/catch.rb b/lib/irb/command/catch.rb
new file mode 100644
index 0000000000..529dcbca5a
--- /dev/null
+++ b/lib/irb/command/catch.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Catch < DebugCommand
+ def execute(arg)
+ execute_debug_command(pre_cmds: "catch #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/chws.rb b/lib/irb/command/chws.rb
new file mode 100644
index 0000000000..ef456d0961
--- /dev/null
+++ b/lib/irb/command/chws.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+#
+# change-ws.rb -
+# by Keiju ISHITSUKA(keiju@ruby-lang.org)
+#
+require_relative "../ext/change-ws"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+
+ class CurrentWorkingWorkspace < Base
+ category "Workspace"
+ description "Show the current workspace."
+
+ def execute(_arg)
+ puts "Current workspace: #{irb_context.main}"
+ end
+ end
+
+ class ChangeWorkspace < Base
+ category "Workspace"
+ description "Change the current workspace to an object."
+
+ def execute(arg)
+ if arg.empty?
+ irb_context.change_workspace
+ else
+ obj = eval(arg, irb_context.workspace.binding)
+ irb_context.change_workspace(obj)
+ end
+
+ puts "Current workspace: #{irb_context.main}"
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/context.rb b/lib/irb/command/context.rb
new file mode 100644
index 0000000000..b4fc807343
--- /dev/null
+++ b/lib/irb/command/context.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module IRB
+ module Command
+ class Context < Base
+ category "IRB"
+ description "Displays current configuration."
+
+ def execute(_arg)
+ # This command just displays the configuration.
+ # Modifying the configuration is achieved by sending a message to IRB.conf.
+ Pager.page_content(IRB.CurrentContext.inspect)
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/continue.rb b/lib/irb/command/continue.rb
new file mode 100644
index 0000000000..0daa029b15
--- /dev/null
+++ b/lib/irb/command/continue.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Continue < DebugCommand
+ def execute(arg)
+ execute_debug_command(do_cmds: "continue #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/debug.rb b/lib/irb/command/debug.rb
new file mode 100644
index 0000000000..f9aca0a672
--- /dev/null
+++ b/lib/irb/command/debug.rb
@@ -0,0 +1,86 @@
+require_relative "../debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Debug < Base
+ category "Debugging"
+ description "Start the debugger of debug.gem."
+
+ BINDING_IRB_FRAME_REGEXPS = [
+ '<internal:prelude>',
+ binding.method(:irb).source_location.first,
+ ].map { |file| /\A#{Regexp.escape(file)}:\d+:in (`|'Binding#)irb'\z/ }
+
+ def execute(_arg)
+ execute_debug_command
+ end
+
+ def execute_debug_command(pre_cmds: nil, do_cmds: nil)
+ pre_cmds = pre_cmds&.rstrip
+ do_cmds = do_cmds&.rstrip
+
+ if irb_context.with_debugger
+ # If IRB is already running with a debug session, throw the command and IRB.debug_readline will pass it to the debugger.
+ if cmd = pre_cmds || do_cmds
+ throw :IRB_EXIT, cmd
+ else
+ puts "IRB is already running with a debug session."
+ return
+ end
+ else
+ # If IRB is not running with a debug session yet, then:
+ # 1. Check if the debugging command is run from a `binding.irb` call.
+ # 2. If so, try setting up the debug gem.
+ # 3. Insert a debug breakpoint at `Irb#debug_break` with the intended command.
+ # 4. Exit the current Irb#run call via `throw :IRB_EXIT`.
+ # 5. `Irb#debug_break` will be called and trigger the breakpoint, which will run the intended command.
+ unless binding_irb?
+ puts "Debugging commands are only available when IRB is started with binding.irb"
+ return
+ end
+
+ if IRB.respond_to?(:JobManager)
+ warn "Can't start the debugger when IRB is running in a multi-IRB session."
+ return
+ end
+
+ 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.
+ MSG
+ return
+ end
+
+ IRB::Debug.insert_debug_break(pre_cmds: pre_cmds, do_cmds: do_cmds)
+
+ # exit current Irb#run call
+ throw :IRB_EXIT
+ end
+ end
+
+ private
+
+ def binding_irb?
+ caller.any? do |frame|
+ BINDING_IRB_FRAME_REGEXPS.any? do |regexp|
+ frame.match?(regexp)
+ end
+ end
+ end
+ end
+
+ class DebugCommand < Debug
+ def self.category
+ "Debugging"
+ end
+
+ def self.description
+ command_name = self.name.split("::").last.downcase
+ "Start the debugger of debug.gem and run its `#{command_name}` command."
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/delete.rb b/lib/irb/command/delete.rb
new file mode 100644
index 0000000000..2a57a4a3de
--- /dev/null
+++ b/lib/irb/command/delete.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Delete < DebugCommand
+ def execute(arg)
+ execute_debug_command(pre_cmds: "delete #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/disable_irb.rb b/lib/irb/command/disable_irb.rb
new file mode 100644
index 0000000000..0b00d0302b
--- /dev/null
+++ b/lib/irb/command/disable_irb.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class DisableIrb < Base
+ category "IRB"
+ description "Disable binding.irb."
+
+ def execute(*)
+ ::Binding.define_method(:irb) {}
+ IRB.irb_exit
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/edit.rb b/lib/irb/command/edit.rb
new file mode 100644
index 0000000000..cb7e0c4873
--- /dev/null
+++ b/lib/irb/command/edit.rb
@@ -0,0 +1,63 @@
+require 'shellwords'
+
+require_relative "../color"
+require_relative "../source_finder"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Edit < Base
+ include RubyArgsExtractor
+
+ category "Misc"
+ description 'Open a file or source location.'
+ help_message <<~HELP_MESSAGE
+ Usage: edit [FILE or constant or method signature]
+
+ Open a file in the editor specified in #{highlight('ENV["VISUAL"]')} or #{highlight('ENV["EDITOR"]')}
+
+ - If no arguments are provided, IRB will attempt to open the file the current context was defined in.
+ - If FILE is provided, IRB will open the file.
+ - If a constant or method signature is provided, IRB will attempt to locate the source file and open it.
+
+ Examples:
+
+ edit
+ edit foo.rb
+ edit Foo
+ edit Foo#bar
+ HELP_MESSAGE
+
+ def execute(arg)
+ # Accept string literal for backward compatibility
+ path = unwrap_string_literal(arg)
+
+ if path.nil?
+ path = @irb_context.irb_path
+ elsif !File.exist?(path)
+ source = SourceFinder.new(@irb_context).find_source(path)
+
+ if source&.file_exist? && !source.binary_file?
+ path = source.file
+ end
+ end
+
+ unless File.exist?(path)
+ puts "Can not find file: #{path}"
+ return
+ end
+
+ if editor = (ENV['VISUAL'] || ENV['EDITOR'])
+ puts "command: '#{editor}'"
+ puts " path: #{path}"
+ system(*Shellwords.split(editor), path)
+ else
+ puts "Can not find editor setting: ENV['VISUAL'] or ENV['EDITOR']"
+ end
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/exit.rb b/lib/irb/command/exit.rb
new file mode 100644
index 0000000000..b4436f0343
--- /dev/null
+++ b/lib/irb/command/exit.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Exit < Base
+ category "IRB"
+ description "Exit the current irb session."
+
+ def execute(_arg)
+ IRB.irb_exit
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/finish.rb b/lib/irb/command/finish.rb
new file mode 100644
index 0000000000..3311a0e6e9
--- /dev/null
+++ b/lib/irb/command/finish.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Finish < DebugCommand
+ def execute(arg)
+ execute_debug_command(do_cmds: "finish #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/force_exit.rb b/lib/irb/command/force_exit.rb
new file mode 100644
index 0000000000..14086aa849
--- /dev/null
+++ b/lib/irb/command/force_exit.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class ForceExit < Base
+ category "IRB"
+ description "Exit the current process."
+
+ def execute(_arg)
+ throw :IRB_EXIT, true
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/help.rb b/lib/irb/command/help.rb
new file mode 100644
index 0000000000..1ed7a7707c
--- /dev/null
+++ b/lib/irb/command/help.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module IRB
+ module Command
+ class Help < Base
+ category "Help"
+ description "List all available commands. Use `help <command>` to get information about a specific command."
+
+ def execute(command_name)
+ content =
+ if command_name.empty?
+ help_message
+ else
+ if command_class = Command.load_command(command_name)
+ command_class.help_message || command_class.description
+ else
+ "Can't find command `#{command_name}`. Please check the command name and try again.\n\n"
+ end
+ end
+ Pager.page_content(content)
+ end
+
+ private
+
+ def help_message
+ commands_info = IRB::Command.all_commands_info
+ helper_methods_info = IRB::HelperMethod.all_helper_methods_info
+ commands_grouped_by_categories = commands_info.group_by { |cmd| cmd[:category] }
+ commands_grouped_by_categories["Helper methods"] = helper_methods_info
+
+ user_aliases = irb_context.instance_variable_get(:@user_aliases)
+
+ commands_grouped_by_categories["Aliases"] = user_aliases.map do |alias_name, target|
+ { display_name: alias_name, description: "Alias for `#{target}`" }
+ end
+
+ if irb_context.with_debugger
+ # Remove the original "Debugging" category
+ commands_grouped_by_categories.delete("Debugging")
+ # Add an empty "Debugging (from debug.gem)" category at the end
+ commands_grouped_by_categories["Debugging (from debug.gem)"] = []
+ end
+
+ longest_cmd_name_length = commands_info.map { |c| c[:display_name].length }.max
+
+ output = StringIO.new
+
+ help_cmds = commands_grouped_by_categories.delete("Help")
+
+ add_category_to_output("Help", help_cmds, output, longest_cmd_name_length)
+
+ commands_grouped_by_categories.each do |category, cmds|
+ add_category_to_output(category, cmds, output, longest_cmd_name_length)
+ end
+
+ # Append the debugger help at the end
+ if irb_context.with_debugger
+ output.puts DEBUGGER__.help
+ end
+
+ output.string
+ end
+
+ def add_category_to_output(category, cmds, output, longest_cmd_name_length)
+ output.puts Color.colorize(category, [:BOLD])
+
+ cmds.each do |cmd|
+ output.puts " #{cmd[:display_name].to_s.ljust(longest_cmd_name_length)} #{cmd[:description]}"
+ end
+
+ output.puts
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/history.rb b/lib/irb/command/history.rb
new file mode 100644
index 0000000000..90f87f9102
--- /dev/null
+++ b/lib/irb/command/history.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "stringio"
+
+require_relative "../pager"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class History < Base
+ category "IRB"
+ description "Shows the input history. `-g [query]` or `-G [query]` allows you to filter the output."
+
+ def execute(arg)
+
+ if (match = arg&.match(/(-g|-G)\s+(?<grep>.+)\s*\n\z/))
+ grep = Regexp.new(match[:grep])
+ end
+
+ formatted_inputs = irb_context.io.class::HISTORY.each_with_index.reverse_each.filter_map do |input, index|
+ next if grep && !input.match?(grep)
+
+ header = "#{index}: "
+
+ first_line, *other_lines = input.split("\n")
+ first_line = "#{header}#{first_line}"
+
+ truncated_lines = other_lines.slice!(1..) # Show 1 additional line (2 total)
+ other_lines << "..." if truncated_lines&.any?
+
+ other_lines.map! do |line|
+ " " * header.length + line
+ end
+
+ [first_line, *other_lines].join("\n") + "\n"
+ end
+
+ Pager.page_content(formatted_inputs.join)
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/info.rb b/lib/irb/command/info.rb
new file mode 100644
index 0000000000..d08ce00a32
--- /dev/null
+++ b/lib/irb/command/info.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Info < DebugCommand
+ def execute(arg)
+ execute_debug_command(pre_cmds: "info #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/internal_helpers.rb b/lib/irb/command/internal_helpers.rb
new file mode 100644
index 0000000000..249b5cdede
--- /dev/null
+++ b/lib/irb/command/internal_helpers.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module IRB
+ module Command
+ # Internal use only, for default command's backward compatibility.
+ module RubyArgsExtractor # :nodoc:
+ def unwrap_string_literal(str)
+ return if str.empty?
+
+ sexp = Ripper.sexp(str)
+ if sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal
+ @irb_context.workspace.binding.eval(str).to_s
+ else
+ str
+ end
+ end
+
+ def ruby_args(arg)
+ # Use throw and catch to handle arg that includes `;`
+ # For example: "1, kw: (2; 3); 4" will be parsed to [[1], { kw: 3 }]
+ catch(:EXTRACT_RUBY_ARGS) do
+ @irb_context.workspace.binding.eval "IRB::Command.extract_ruby_args #{arg}"
+ end || [[], {}]
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/irb_info.rb b/lib/irb/command/irb_info.rb
new file mode 100644
index 0000000000..6d868de94c
--- /dev/null
+++ b/lib/irb/command/irb_info.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class IrbInfo < Base
+ category "IRB"
+ description "Show information about IRB."
+
+ def execute(_arg)
+ str = "Ruby version: #{RUBY_VERSION}\n"
+ str += "IRB version: #{IRB.version}\n"
+ str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n"
+ str += "Completion: #{IRB.CurrentContext.io.respond_to?(:completion_info) ? IRB.CurrentContext.io.completion_info : 'off'}\n"
+ rc_files = IRB.irbrc_files
+ str += ".irbrc paths: #{rc_files.join(", ")}\n" if rc_files.any?
+ str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n"
+ str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty?
+ str += "LC_ALL env: #{ENV["LC_ALL"]}\n" if ENV["LC_ALL"] && !ENV["LC_ALL"].empty?
+ str += "East Asian Ambiguous Width: #{Reline.ambiguous_width.inspect}\n"
+ if RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
+ codepage = `chcp`.b.sub(/.*: (\d+)\n/, '\1')
+ str += "Code page: #{codepage}\n"
+ end
+ puts str
+ nil
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/load.rb b/lib/irb/command/load.rb
new file mode 100644
index 0000000000..1cd3f279d1
--- /dev/null
+++ b/lib/irb/command/load.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+#
+# load.rb -
+# by Keiju ISHITSUKA(keiju@ruby-lang.org)
+#
+require_relative "../ext/loader"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class LoaderCommand < Base
+ include RubyArgsExtractor
+ include IrbLoader
+
+ def raise_cmd_argument_error
+ raise CommandArgumentError.new("Please specify the file name.")
+ end
+ end
+
+ class Load < LoaderCommand
+ category "IRB"
+ description "Load a Ruby file."
+
+ def execute(arg)
+ args, kwargs = ruby_args(arg)
+ execute_internal(*args, **kwargs)
+ end
+
+ def execute_internal(file_name = nil, priv = nil)
+ raise_cmd_argument_error unless file_name
+ irb_load(file_name, priv)
+ end
+ end
+
+ class Require < LoaderCommand
+ category "IRB"
+ description "Require a Ruby file."
+
+ def execute(arg)
+ args, kwargs = ruby_args(arg)
+ execute_internal(*args, **kwargs)
+ end
+
+ def execute_internal(file_name = nil)
+ raise_cmd_argument_error unless file_name
+
+ rex = Regexp.new("#{Regexp.quote(file_name)}(\.o|\.rb)?")
+ return false if $".find{|f| f =~ rex}
+
+ case file_name
+ when /\.rb$/
+ begin
+ if irb_load(file_name)
+ $".push file_name
+ return true
+ end
+ rescue LoadError
+ end
+ when /\.(so|o|sl)$/
+ return ruby_require(file_name)
+ end
+
+ begin
+ irb_load(f = file_name + ".rb")
+ $".push f
+ return true
+ rescue LoadError
+ return ruby_require(file_name)
+ end
+ end
+ end
+
+ class Source < LoaderCommand
+ category "IRB"
+ description "Loads a given file in the current session."
+
+ def execute(arg)
+ args, kwargs = ruby_args(arg)
+ execute_internal(*args, **kwargs)
+ end
+
+ def execute_internal(file_name = nil)
+ raise_cmd_argument_error unless file_name
+
+ source_file(file_name)
+ end
+ end
+ end
+ # :startdoc:
+end
diff --git a/lib/irb/cmd/ls.rb b/lib/irb/command/ls.rb
index f4a7348bd1..cbd9998bc4 100644
--- a/lib/irb/cmd/ls.rb
+++ b/lib/irb/command/ls.rb
@@ -1,45 +1,92 @@
# frozen_string_literal: true
require "reline"
-require_relative "nop"
+require "stringio"
+
+require_relative "../pager"
require_relative "../color"
module IRB
# :stopdoc:
- module ExtendCommand
- class Ls < Nop
- def execute(*arg, grep: nil)
+ module Command
+ class Ls < Base
+ include RubyArgsExtractor
+
+ category "Context"
+ description "Show methods, constants, and variables."
+
+ help_message <<~HELP_MESSAGE
+ Usage: ls [obj] [-g [query]]
+
+ -g [query] Filter the output with a query.
+ HELP_MESSAGE
+
+ def execute(arg)
+ if match = arg.match(/\A(?<target>.+\s|)(-g|-G)\s+(?<grep>.+)$/)
+ if match[:target].empty?
+ use_main = true
+ else
+ obj = @irb_context.workspace.binding.eval(match[:target])
+ end
+ grep = Regexp.new(match[:grep])
+ else
+ args, kwargs = ruby_args(arg)
+ use_main = args.empty?
+ obj = args.first
+ grep = kwargs[:grep]
+ end
+
+ if use_main
+ obj = irb_context.workspace.main
+ locals = irb_context.workspace.binding.local_variables
+ end
+
o = Output.new(grep: grep)
- obj = arg.empty? ? irb_context.workspace.main : arg.first
- locals = arg.empty? ? irb_context.workspace.binding.local_variables : []
klass = (obj.class == Class || obj.class == Module ? obj : obj.class)
o.dump("constants", obj.constants) if obj.respond_to?(:constants)
dump_methods(o, klass, obj)
o.dump("instance variables", obj.instance_variables)
o.dump("class variables", klass.class_variables)
- o.dump("locals", locals)
+ o.dump("locals", locals) if locals
+ o.print_result
end
def dump_methods(o, klass, obj)
singleton_class = begin obj.singleton_class; rescue TypeError; nil end
- maps = class_method_map((singleton_class || klass).ancestors)
+ dumped_mods = Array.new
+ ancestors = klass.ancestors
+ ancestors = ancestors.reject { |c| c >= Object } if klass < Object
+ singleton_ancestors = (singleton_class&.ancestors || []).reject { |c| c >= Class }
+
+ # singleton_class' ancestors should be at the front
+ maps = class_method_map(singleton_ancestors, dumped_mods) + class_method_map(ancestors, dumped_mods)
maps.each do |mod, methods|
name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods"
o.dump(name, methods)
end
end
- def class_method_map(classes)
- dumped = Array.new
- classes.reject { |mod| mod >= Object }.map do |mod|
- methods = mod.public_instance_methods(false).select do |m|
- dumped.push(m) unless dumped.include?(m)
+ def class_method_map(classes, dumped_mods)
+ dumped_methods = Array.new
+ classes.map do |mod|
+ next if dumped_mods.include? mod
+
+ dumped_mods << mod
+
+ methods = mod.public_instance_methods(false).select do |method|
+ if dumped_methods.include? method
+ false
+ else
+ dumped_methods << method
+ true
+ end
end
+
[mod, methods]
- end.reverse
+ end.compact
end
class Output
@@ -48,6 +95,11 @@ module IRB
def initialize(grep: nil)
@grep = grep
@line_width = screen_width - MARGIN.length # right padding
+ @io = StringIO.new
+ end
+
+ def print_result
+ Pager.page_content(@io.string)
end
def dump(name, strs)
@@ -56,12 +108,12 @@ module IRB
return if strs.empty?
# Attempt a single line
- print "#{Color.colorize(name, [:BOLD, :BLUE])}: "
+ @io.print "#{Color.colorize(name, [:BOLD, :BLUE])}: "
if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length)
- puts strs.join(MARGIN)
+ @io.puts strs.join(MARGIN)
return
end
- puts
+ @io.puts
# Dump with the largest # of columns that fits on a line
cols = strs.size
@@ -70,7 +122,7 @@ module IRB
end
widths = col_widths(strs, cols: cols)
strs.each_slice(cols) do |ss|
- puts ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join
+ @io.puts ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join
end
end
diff --git a/lib/irb/cmd/measure.rb b/lib/irb/command/measure.rb
index a97baee9f1..f96be20de8 100644
--- a/lib/irb/cmd/measure.rb
+++ b/lib/irb/command/measure.rb
@@ -1,40 +1,44 @@
-require_relative "nop"
-
module IRB
# :stopdoc:
- module ExtendCommand
- class Measure < Nop
+ module Command
+ class Measure < Base
+ include RubyArgsExtractor
+
+ category "Misc"
+ description "`measure` enables the mode to measure processing time. `measure :off` disables it."
+
def initialize(*args)
super(*args)
end
- def execute(type = nil, arg = nil, &block)
+ def execute(arg)
+ if arg&.match?(/^do$|^do[^\w]|^\{/)
+ warn 'Configure IRB.conf[:MEASURE_PROC] to add custom measure methods.'
+ return
+ end
+ args, kwargs = ruby_args(arg)
+ execute_internal(*args, **kwargs)
+ end
+
+ def execute_internal(type = nil, arg = nil)
# Please check IRB.init_config in lib/irb/init.rb that sets
# IRB.conf[:MEASURE_PROC] to register default "measure" methods,
# "measure :time" (abbreviated as "measure") and "measure :stackprof".
+
case type
when :off
- IRB.conf[:MEASURE] = nil
IRB.unset_measure_callback(arg)
when :list
IRB.conf[:MEASURE_CALLBACKS].each do |type_name, _, arg_val|
puts "- #{type_name}" + (arg_val ? "(#{arg_val.inspect})" : '')
end
when :on
- IRB.conf[:MEASURE] = true
- added = IRB.set_measure_callback(type, arg)
+ added = IRB.set_measure_callback(arg)
puts "#{added[0]} is added." if added
else
- if block_given?
- IRB.conf[:MEASURE] = true
- added = IRB.set_measure_callback(&block)
- puts "#{added[0]} is added." if added
- else
- IRB.conf[:MEASURE] = true
- added = IRB.set_measure_callback(type, arg)
- puts "#{added[0]} is added." if added
- end
+ added = IRB.set_measure_callback(type, arg)
+ puts "#{added[0]} is added." if added
end
nil
end
diff --git a/lib/irb/command/next.rb b/lib/irb/command/next.rb
new file mode 100644
index 0000000000..3fc6b68d21
--- /dev/null
+++ b/lib/irb/command/next.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Next < DebugCommand
+ def execute(arg)
+ execute_debug_command(do_cmds: "next #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/pushws.rb b/lib/irb/command/pushws.rb
new file mode 100644
index 0000000000..b51928c650
--- /dev/null
+++ b/lib/irb/command/pushws.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+#
+# change-ws.rb -
+# by Keiju ISHITSUKA(keiju@ruby-lang.org)
+#
+
+require_relative "../ext/workspaces"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Workspaces < Base
+ category "Workspace"
+ description "Show workspaces."
+
+ def execute(_arg)
+ inspection_resuls = irb_context.instance_variable_get(:@workspace_stack).map do |ws|
+ truncated_inspect(ws.main)
+ end
+
+ puts "[" + inspection_resuls.join(", ") + "]"
+ end
+
+ private
+
+ def truncated_inspect(obj)
+ obj_inspection = obj.inspect
+
+ if obj_inspection.size > 20
+ obj_inspection = obj_inspection[0, 19] + "...>"
+ end
+
+ obj_inspection
+ end
+ end
+
+ class PushWorkspace < Workspaces
+ category "Workspace"
+ description "Push an object to the workspace stack."
+
+ def execute(arg)
+ if arg.empty?
+ irb_context.push_workspace
+ else
+ obj = eval(arg, irb_context.workspace.binding)
+ irb_context.push_workspace(obj)
+ end
+ super
+ end
+ end
+
+ class PopWorkspace < Workspaces
+ category "Workspace"
+ description "Pop a workspace from the workspace stack."
+
+ def execute(_arg)
+ irb_context.pop_workspace
+ super
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/show_doc.rb b/lib/irb/command/show_doc.rb
new file mode 100644
index 0000000000..8a2188e4eb
--- /dev/null
+++ b/lib/irb/command/show_doc.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module IRB
+ module Command
+ class ShowDoc < Base
+ include RubyArgsExtractor
+
+ category "Context"
+ description "Look up documentation with RI."
+
+ help_message <<~HELP_MESSAGE
+ Usage: show_doc [name]
+
+ When name is provided, IRB will look up the documentation for the given name.
+ When no name is provided, a RI session will be started.
+
+ Examples:
+
+ show_doc
+ show_doc Array
+ show_doc Array#each
+
+ HELP_MESSAGE
+
+ def execute(arg)
+ # Accept string literal for backward compatibility
+ name = unwrap_string_literal(arg)
+ require 'rdoc/ri/driver'
+
+ unless ShowDoc.const_defined?(:Ri)
+ opts = RDoc::RI::Driver.process_args([])
+ ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts))
+ end
+
+ if name.nil?
+ Ri.interactive
+ else
+ begin
+ Ri.display_name(name)
+ rescue RDoc::RI::Error
+ puts $!.message
+ end
+ end
+
+ nil
+ rescue LoadError, SystemExit
+ warn "Can't display document because `rdoc` is not installed."
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/show_source.rb b/lib/irb/command/show_source.rb
new file mode 100644
index 0000000000..f4c6f104a2
--- /dev/null
+++ b/lib/irb/command/show_source.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require_relative "../source_finder"
+require_relative "../pager"
+require_relative "../color"
+
+module IRB
+ module Command
+ class ShowSource < Base
+ include RubyArgsExtractor
+
+ category "Context"
+ description "Show the source code of a given method, class/module, or constant."
+
+ help_message <<~HELP_MESSAGE
+ Usage: show_source [target] [-s]
+
+ -s Show the super method. You can stack it like `-ss` to show the super of the super, etc.
+
+ Examples:
+
+ show_source Foo
+ show_source Foo#bar
+ show_source Foo#bar -s
+ show_source Foo.baz
+ show_source Foo::BAR
+ HELP_MESSAGE
+
+ def execute(arg)
+ # Accept string literal for backward compatibility
+ str = unwrap_string_literal(arg)
+ unless str.is_a?(String)
+ puts "Error: Expected a string but got #{str.inspect}"
+ return
+ end
+
+ str, esses = str.split(" -")
+ super_level = esses ? esses.count("s") : 0
+ source = SourceFinder.new(@irb_context).find_source(str, super_level)
+
+ if source
+ show_source(source)
+ elsif super_level > 0
+ puts "Error: Couldn't locate a super definition for #{str}"
+ else
+ puts "Error: Couldn't locate a definition for #{str}"
+ end
+ nil
+ end
+
+ private
+
+ def show_source(source)
+ if source.binary_file?
+ content = "\n#{bold('Defined in binary file')}: #{source.file}\n\n"
+ else
+ code = source.colorized_content || 'Source not available'
+ content = <<~CONTENT
+
+ #{bold("From")}: #{source.file}:#{source.line}
+
+ #{code.chomp}
+
+ CONTENT
+ end
+ Pager.page_content(content)
+ end
+
+ def bold(str)
+ Color.colorize(str, [:BOLD])
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/step.rb b/lib/irb/command/step.rb
new file mode 100644
index 0000000000..29e5e35ac0
--- /dev/null
+++ b/lib/irb/command/step.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "debug"
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class Step < DebugCommand
+ def execute(arg)
+ execute_debug_command(do_cmds: "step #{arg}")
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/command/subirb.rb b/lib/irb/command/subirb.rb
new file mode 100644
index 0000000000..85af28c1a5
--- /dev/null
+++ b/lib/irb/command/subirb.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+#
+# multi.rb -
+# by Keiju ISHITSUKA(keiju@ruby-lang.org)
+#
+
+module IRB
+ # :stopdoc:
+
+ module Command
+ class MultiIRBCommand < Base
+ include RubyArgsExtractor
+
+ private
+
+ def print_deprecated_warning
+ warn <<~MSG
+ Multi-irb commands are deprecated and will be removed in IRB 2.0.0. Please use workspace commands instead.
+ If you have any use case for multi-irb, please leave a comment at https://github.com/ruby/irb/issues/653
+ MSG
+ end
+
+ def extend_irb_context
+ # this extension patches IRB context like IRB.CurrentContext
+ require_relative "../ext/multi-irb"
+ end
+
+ def print_debugger_warning
+ warn "Multi-IRB commands are not available when the debugger is enabled."
+ end
+ end
+
+ class IrbCommand < MultiIRBCommand
+ category "Multi-irb (DEPRECATED)"
+ description "Start a child IRB."
+
+ def execute(arg)
+ args, kwargs = ruby_args(arg)
+ execute_internal(*args, **kwargs)
+ end
+
+ def execute_internal(*obj)
+ print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ extend_irb_context
+ IRB.irb(nil, *obj)
+ puts IRB.JobManager.inspect
+ end
+ end
+
+ class Jobs < MultiIRBCommand
+ category "Multi-irb (DEPRECATED)"
+ description "List of current sessions."
+
+ def execute(_arg)
+ print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ extend_irb_context
+ puts IRB.JobManager.inspect
+ end
+ end
+
+ class Foreground < MultiIRBCommand
+ category "Multi-irb (DEPRECATED)"
+ description "Switches to the session of the given number."
+
+ def execute(arg)
+ args, kwargs = ruby_args(arg)
+ execute_internal(*args, **kwargs)
+ end
+
+ def execute_internal(key = nil)
+ print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ extend_irb_context
+
+ raise CommandArgumentError.new("Please specify the id of target IRB job (listed in the `jobs` command).") unless key
+ IRB.JobManager.switch(key)
+ puts IRB.JobManager.inspect
+ end
+ end
+
+ class Kill < MultiIRBCommand
+ category "Multi-irb (DEPRECATED)"
+ description "Kills the session with the given number."
+
+ def execute(arg)
+ args, kwargs = ruby_args(arg)
+ execute_internal(*args, **kwargs)
+ end
+
+ def execute_internal(*keys)
+ print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ extend_irb_context
+ IRB.JobManager.kill(*keys)
+ puts IRB.JobManager.inspect
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/irb/cmd/whereami.rb b/lib/irb/command/whereami.rb
index b8c7e047fa..c8439f1212 100644
--- a/lib/irb/cmd/whereami.rb
+++ b/lib/irb/command/whereami.rb
@@ -1,13 +1,14 @@
# frozen_string_literal: true
-require_relative "nop"
-
module IRB
# :stopdoc:
- module ExtendCommand
- class Whereami < Nop
- def execute(*)
+ module Command
+ class Whereami < Base
+ category "Context"
+ description "Show the source code around binding.irb again."
+
+ def execute(_arg)
code = irb_context.workspace.code_around_binding
if code
puts code
diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb
index 9121174a50..a3d89373c3 100644
--- a/lib/irb/completion.rb
+++ b/lib/irb/completion.rb
@@ -1,8 +1,6 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/completion.rb -
-# $Release Version: 0.9$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ishitsuka.com)
# From Original Idea of shugo@ruby-lang.org
#
@@ -10,8 +8,7 @@
require_relative 'ruby-lex'
module IRB
- module InputCompletor # :nodoc:
-
+ class BaseCompletor # :nodoc:
# Set of reserved words used by Ruby, you should not use these for
# constants or variables
@@ -36,35 +33,42 @@ module IRB
yield
]
- BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{("
+ def completion_candidates(preposing, target, postposing, bind:)
+ raise NotImplementedError
+ end
+
+ def doc_namespace(preposing, matched, postposing, bind:)
+ raise NotImplementedError
+ end
- def self.absolute_path?(p) # TODO Remove this method after 2.6 EOL.
- if File.respond_to?(:absolute_path?)
- File.absolute_path?(p)
+ GEM_PATHS =
+ if defined?(Gem::Specification)
+ Gem::Specification.latest_specs(true).map { |s|
+ s.require_paths.map { |p|
+ if File.absolute_path?(p)
+ p
+ else
+ File.join(s.full_gem_path, p)
+ end
+ }
+ }.flatten
else
- if File.absolute_path(p) == p
- true
+ []
+ end.freeze
+
+ def retrieve_gem_and_system_load_path
+ candidates = (GEM_PATHS | $LOAD_PATH)
+ candidates.map do |p|
+ if p.respond_to?(:to_path)
+ p.to_path
else
- false
+ String(p) rescue nil
end
- end
+ end.compact.sort
end
- def self.retrieve_gem_and_system_load_path
- gem_paths = Gem::Specification.latest_specs(true).map { |s|
- s.require_paths.map { |p|
- if absolute_path?(p)
- p
- else
- File.join(s.full_gem_path, p)
- end
- }
- }.flatten if defined?(Gem::Specification)
- (gem_paths.to_a | $LOAD_PATH).sort
- end
-
- def self.retrieve_files_to_require_from_load_path
- @@files_from_load_path ||=
+ def retrieve_files_to_require_from_load_path
+ @files_from_load_path ||=
(
shortest = []
rest = retrieve_gem_and_system_load_path.each_with_object([]) { |path, result|
@@ -82,13 +86,74 @@ module IRB
)
end
- def self.retrieve_files_to_require_relative_from_current_dir
- @@files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path|
+ def command_completions(preposing, target)
+ if preposing.empty? && !target.empty?
+ IRB::Command.command_names.select { _1.start_with?(target) }
+ else
+ []
+ end
+ end
+
+ def retrieve_files_to_require_relative_from_current_dir
+ @files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path|
path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '')
}
end
+ end
+
+ class TypeCompletor < BaseCompletor # :nodoc:
+ def initialize(context)
+ @context = context
+ end
+
+ def inspect
+ ReplTypeCompletor.info
+ end
+
+ def completion_candidates(preposing, target, _postposing, bind:)
+ commands = command_completions(preposing, target)
+ result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path)
+ return commands unless result
+
+ commands | result.completion_candidates.map { target + _1 }
+ end
+
+ def doc_namespace(preposing, matched, _postposing, bind:)
+ result = ReplTypeCompletor.analyze(preposing + matched, binding: bind, filename: @context.irb_path)
+ result&.doc_namespace('')
+ end
+ end
+
+ class RegexpCompletor < BaseCompletor # :nodoc:
+ using Module.new {
+ refine ::Binding do
+ def eval_methods
+ ::Kernel.instance_method(:methods).bind(eval("self")).call
+ end
+
+ def eval_private_methods
+ ::Kernel.instance_method(:private_methods).bind(eval("self")).call
+ end
+
+ def eval_instance_variables
+ ::Kernel.instance_method(:instance_variables).bind(eval("self")).call
+ end
+
+ def eval_global_variables
+ ::Kernel.instance_method(:global_variables).bind(eval("self")).call
+ end
+
+ def eval_class_constants
+ ::Module.instance_method(:constants).bind(eval("self.class")).call
+ end
+ end
+ }
- CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil|
+ def inspect
+ 'RegexpCompletor'
+ end
+
+ def complete_require_path(target, preposing, postposing)
if target =~ /\A(['"])([^'"]+)\Z/
quote = $1
actual_target = $2
@@ -103,61 +168,64 @@ module IRB
break
end
end
- result = []
- if tok && tok.event == :on_ident && tok.state == Ripper::EXPR_CMDARG
- case tok.tok
- when 'require'
- result = retrieve_files_to_require_from_load_path.select { |path|
- path.start_with?(actual_target)
- }.map { |path|
- quote + path
- }
- when 'require_relative'
- result = retrieve_files_to_require_relative_from_current_dir.select { |path|
- path.start_with?(actual_target)
- }.map { |path|
- quote + path
- }
- end
+ return unless tok&.event == :on_ident && tok.state == Ripper::EXPR_CMDARG
+
+ case tok.tok
+ when 'require'
+ retrieve_files_to_require_from_load_path.select { |path|
+ path.start_with?(actual_target)
+ }.map { |path|
+ quote + path
+ }
+ when 'require_relative'
+ retrieve_files_to_require_relative_from_current_dir.select { |path|
+ path.start_with?(actual_target)
+ }.map { |path|
+ quote + path
+ }
end
- result
- }
+ end
- CompletionProc = lambda { |target, preposing = nil, postposing = nil|
+ def completion_candidates(preposing, target, postposing, bind:)
if preposing && postposing
- result = CompletionRequireProc.(target, preposing, postposing)
- unless result
- result = retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) }
- end
- result
- else
- retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) }
+ result = complete_require_path(target, preposing, postposing)
+ return result if result
end
- }
+ commands = command_completions(preposing || '', target)
+ commands | retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) }
+ end
- def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false)
+ def doc_namespace(_preposing, matched, _postposing, bind:)
+ retrieve_completion_data(matched, bind: bind, doc_namespace: true)
+ end
+
+ def retrieve_completion_data(input, bind:, doc_namespace:)
case input
- when /^((["'`]).*\2)\.([^.]*)$/
+ # this regexp only matches the closing character because of irb's Reline.completer_quote_characters setting
+ # details are described in: https://github.com/ruby/irb/pull/523
+ when /^(.*["'`])\.([^.]*)$/
# String
receiver = $1
- message = $3
+ message = $2
- candidates = String.instance_methods.collect{|m| m.to_s}
if doc_namespace
"String.#{message}"
else
+ candidates = String.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates)
end
- when /^(\/[^\/]*\/)\.([^.]*)$/
+ # this regexp only matches the closing character because of irb's Reline.completer_quote_characters setting
+ # details are described in: https://github.com/ruby/irb/pull/523
+ when /^(.*\/)\.([^.]*)$/
# Regexp
receiver = $1
message = $2
- candidates = Regexp.instance_methods.collect{|m| m.to_s}
if doc_namespace
"Regexp.#{message}"
else
+ candidates = Regexp.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates)
end
@@ -166,61 +234,67 @@ module IRB
receiver = $1
message = $2
- candidates = Array.instance_methods.collect{|m| m.to_s}
if doc_namespace
"Array.#{message}"
else
+ candidates = Array.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates)
end
when /^([^\}]*\})\.([^.]*)$/
- # Proc or Hash
+ # Hash or Proc
receiver = $1
message = $2
- proc_candidates = Proc.instance_methods.collect{|m| m.to_s}
- hash_candidates = Hash.instance_methods.collect{|m| m.to_s}
if doc_namespace
- ["Proc.#{message}", "Hash.#{message}"]
+ ["Hash.#{message}", "Proc.#{message}"]
else
- select_message(receiver, message, proc_candidates | hash_candidates)
+ hash_candidates = Hash.instance_methods.collect{|m| m.to_s}
+ proc_candidates = Proc.instance_methods.collect{|m| m.to_s}
+ select_message(receiver, message, hash_candidates | proc_candidates)
end
- when /^(:[^:.]*)$/
+ when /^(:[^:.]+)$/
# Symbol
- return nil if doc_namespace
- sym = $1
- candidates = Symbol.all_symbols.collect do |s|
- ":" + s.id2name.encode(Encoding.default_external)
- rescue EncodingError
- # ignore
+ if doc_namespace
+ nil
+ else
+ sym = $1
+ candidates = Symbol.all_symbols.collect do |s|
+ s.inspect
+ rescue EncodingError
+ # ignore
+ end
+ candidates.grep(/^#{Regexp.quote(sym)}/)
end
- candidates.grep(/^#{Regexp.quote(sym)}/)
-
when /^::([A-Z][^:\.\(\)]*)$/
# Absolute Constant or class methods
receiver = $1
+
candidates = Object.constants.collect{|m| m.to_s}
+
if doc_namespace
candidates.find { |i| i == receiver }
else
- candidates.grep(/^#{receiver}/).collect{|e| "::" + e}
+ candidates.grep(/^#{Regexp.quote(receiver)}/).collect{|e| "::" + e}
end
when /^([A-Z].*)::([^:.]*)$/
# Constant or class methods
receiver = $1
message = $2
- begin
- candidates = eval("#{receiver}.constants.collect{|m| m.to_s}", bind)
- candidates |= eval("#{receiver}.methods.collect{|m| m.to_s}", bind)
- rescue Exception
- candidates = []
- end
+
if doc_namespace
"#{receiver}::#{message}"
else
- select_message(receiver, message, candidates, "::")
+ begin
+ candidates = eval("#{receiver}.constants.collect{|m| m.to_s}", bind)
+ candidates |= eval("#{receiver}.methods.collect{|m| m.to_s}", bind)
+ rescue Exception
+ candidates = []
+ end
+
+ select_message(receiver, message, candidates.sort, "::")
end
when /^(:[^:.]+)(\.|::)([^.]*)$/
@@ -229,10 +303,10 @@ module IRB
sep = $2
message = $3
- candidates = Symbol.instance_methods.collect{|m| m.to_s}
if doc_namespace
"Symbol.#{message}"
else
+ candidates = Symbol.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates, sep)
end
@@ -244,6 +318,7 @@ module IRB
begin
instance = eval(receiver, bind)
+
if doc_namespace
"#{instance.class.name}.#{message}"
else
@@ -254,7 +329,7 @@ module IRB
if doc_namespace
nil
else
- candidates = []
+ []
end
end
@@ -276,7 +351,7 @@ module IRB
if doc_namespace
nil
else
- candidates = []
+ []
end
end
@@ -284,6 +359,7 @@ module IRB
# global var
gvar = $1
all_gvars = global_variables.collect{|m| m.to_s}
+
if doc_namespace
all_gvars.find{ |i| i == gvar }
else
@@ -296,10 +372,10 @@ module IRB
sep = $2
message = $3
- gv = eval("global_variables", bind).collect{|m| m.to_s}.push("true", "false", "nil")
- lv = eval("local_variables", bind).collect{|m| m.to_s}
- iv = eval("instance_variables", bind).collect{|m| m.to_s}
- cv = eval("self.class.constants", bind).collect{|m| m.to_s}
+ gv = bind.eval_global_variables.collect{|m| m.to_s}.push("true", "false", "nil")
+ lv = bind.local_variables.collect{|m| m.to_s}
+ iv = bind.eval_instance_variables.collect{|m| m.to_s}
+ cv = bind.eval_class_constants.collect{|m| m.to_s}
if (gv | lv | iv | cv).include?(receiver) or /^[A-Z]/ =~ receiver && /\./ !~ receiver
# foo.func and foo is var. OR
@@ -319,17 +395,11 @@ module IRB
else
# func1.func2
candidates = []
- to_ignore = ignored_modules
- ObjectSpace.each_object(Module){|m|
- next if (to_ignore.include?(m) rescue true)
- candidates.concat m.instance_methods(false).collect{|x| x.to_s}
- }
- candidates.sort!
- candidates.uniq!
end
+
if doc_namespace
rec_class = rec.is_a?(Module) ? rec : rec.class
- "#{rec_class.name}#{sep}#{candidates.find{ |i| i == message }}"
+ "#{rec_class.name}#{sep}#{candidates.find{ |i| i == message }}" rescue nil
else
select_message(receiver, message, candidates, sep)
end
@@ -341,69 +411,42 @@ module IRB
message = $1
candidates = String.instance_methods(true).collect{|m| m.to_s}
+
if doc_namespace
"String.#{candidates.find{ |i| i == message }}"
else
- select_message(receiver, message, candidates)
+ select_message(receiver, message, candidates.sort)
+ end
+ when /^\s*$/
+ # empty input
+ if doc_namespace
+ nil
+ else
+ []
end
-
else
if doc_namespace
- vars = eval("local_variables | instance_variables", bind).collect{|m| m.to_s}
+ vars = (bind.local_variables | bind.eval_instance_variables).collect{|m| m.to_s}
perfect_match_var = vars.find{|m| m.to_s == input}
if perfect_match_var
- eval("#{perfect_match_var}.class.name", bind)
+ eval("#{perfect_match_var}.class.name", bind) rescue nil
else
- candidates = eval("methods | private_methods | local_variables | instance_variables | self.class.constants", bind).collect{|m| m.to_s}
+ candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
candidates |= ReservedWords
candidates.find{ |i| i == input }
end
else
- candidates = eval("methods | private_methods | local_variables | instance_variables | self.class.constants", bind).collect{|m| m.to_s}
+ candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
candidates |= ReservedWords
- candidates.grep(/^#{Regexp.quote(input)}/)
+ candidates.grep(/^#{Regexp.quote(input)}/).sort
end
end
end
- PerfectMatchedProc = ->(matched, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding) {
- begin
- require 'rdoc'
- rescue LoadError
- return
- end
-
- RDocRIDriver ||= RDoc::RI::Driver.new
-
- if matched =~ /\A(?:::)?RubyVM/ and not ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER']
- IRB.__send__(:easter_egg)
- return
- end
-
- namespace = retrieve_completion_data(matched, bind: bind, doc_namespace: true)
- return unless namespace
-
- if namespace.is_a?(Array)
- out = RDoc::Markup::Document.new
- namespace.each do |m|
- begin
- RDocRIDriver.add_method(out, m)
- rescue RDoc::RI::Driver::NotFoundError
- end
- end
- RDocRIDriver.display(out)
- else
- begin
- RDocRIDriver.display_names([namespace])
- rescue RDoc::RI::Driver::NotFoundError
- end
- end
- }
-
# Set of available operators in Ruby
Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~]
- def self.select_message(receiver, message, candidates, sep = ".")
+ def select_message(receiver, message, candidates, sep = ".")
candidates.grep(/^#{Regexp.quote(message)}/).collect do |e|
case e
when /^[a-zA-Z_]/
@@ -414,30 +457,21 @@ module IRB
end
end
end
+ end
- def self.ignored_modules
- # We could cache the result, but this is very fast already.
- # By using this approach, we avoid Module#name calls, which are
- # relatively slow when there are a lot of anonymous modules defined.
- s = {}
-
- scanner = lambda do |m|
- next if s.include?(m) # IRB::ExtendCommandBundle::EXCB recurses.
- s[m] = true
- m.constants(false).each do |c|
- value = m.const_get(c)
- scanner.call(value) if value.is_a?(Module)
- end
+ module InputCompletor
+ class << self
+ private def regexp_completor
+ @regexp_completor ||= RegexpCompletor.new
end
- %i(IRB RubyLex).each do |sym|
- next unless Object.const_defined?(sym)
- scanner.call(Object.const_get(sym))
+ def retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false)
+ regexp_completor.retrieve_completion_data(input, bind: bind, doc_namespace: doc_namespace)
end
-
- s.delete(IRB::Context) if defined?(IRB::Context)
-
- s
end
+ CompletionProc = ->(target, preposing = nil, postposing = nil) {
+ regexp_completor.completion_candidates(preposing, target, postposing, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding)
+ }
end
+ deprecate_constant :InputCompletor
end
diff --git a/lib/irb/context.rb b/lib/irb/context.rb
index e6c993d423..22e855f1ef 100644
--- a/lib/irb/context.rb
+++ b/lib/irb/context.rb
@@ -1,14 +1,9 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/context.rb - irb context
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
+
require_relative "workspace"
require_relative "inspector"
require_relative "input-method"
@@ -22,17 +17,18 @@ module IRB
#
# The optional +input_method+ argument:
#
- # +nil+:: uses stdin or Reidline or Readline
+ # +nil+:: uses stdin or Reline or Readline
# +String+:: uses a File
# +other+:: uses this as InputMethod
def initialize(irb, workspace = nil, input_method = nil)
@irb = irb
+ @workspace_stack = []
if workspace
- @workspace = workspace
+ @workspace_stack << workspace
else
- @workspace = WorkSpace.new
+ @workspace_stack << WorkSpace.new
end
- @thread = Thread.current if defined? Thread
+ @thread = Thread.current
# copy of default configuration
@ap_name = IRB.conf[:AP_NAME]
@@ -48,7 +44,15 @@ module IRB
end
if IRB.conf.has_key?(:USE_MULTILINE)
@use_multiline = IRB.conf[:USE_MULTILINE]
- elsif IRB.conf.has_key?(:USE_REIDLINE) # backward compatibility
+ elsif IRB.conf.has_key?(:USE_RELINE) # backward compatibility
+ warn <<~MSG.strip
+ USE_RELINE is deprecated, please use USE_MULTILINE instead.
+ MSG
+ @use_multiline = IRB.conf[:USE_RELINE]
+ elsif IRB.conf.has_key?(:USE_REIDLINE)
+ warn <<~MSG.strip
+ USE_REIDLINE is deprecated, please use USE_MULTILINE instead.
+ MSG
@use_multiline = IRB.conf[:USE_REIDLINE]
else
@use_multiline = nil
@@ -58,7 +62,7 @@ module IRB
@io = nil
self.inspect_mode = IRB.conf[:INSPECT_MODE]
- self.use_tracer = IRB.conf[:USE_TRACER] if IRB.conf[:USE_TRACER]
+ self.use_tracer = IRB.conf[:USE_TRACER]
self.use_loader = IRB.conf[:USE_LOADER] if IRB.conf[:USE_LOADER]
self.eval_history = IRB.conf[:EVAL_HISTORY] if IRB.conf[:EVAL_HISTORY]
@@ -74,7 +78,7 @@ module IRB
else
@irb_name = IRB.conf[:IRB_NAME]+"#"+IRB.JobManager.n_jobs.to_s
end
- @irb_path = "(" + @irb_name + ")"
+ self.irb_path = "(" + @irb_name + ")"
case input_method
when nil
@@ -83,14 +87,14 @@ module IRB
when nil
if STDIN.tty? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline?
# Both of multiline mode and singleline mode aren't specified.
- @io = ReidlineInputMethod.new
+ @io = RelineInputMethod.new(build_completor)
else
@io = nil
end
when false
@io = nil
when true
- @io = ReidlineInputMethod.new
+ @io = RelineInputMethod.new(build_completor)
end
unless @io
case use_singleline?
@@ -115,15 +119,17 @@ module IRB
end
@io = StdioInputMethod.new unless @io
+ when '-'
+ @io = FileInputMethod.new($stdin)
+ @irb_name = '-'
+ self.irb_path = '-'
when String
@io = FileInputMethod.new(input_method)
@irb_name = File.basename(input_method)
- @irb_path = input_method
+ self.irb_path = input_method
else
@io = input_method
end
- self.save_history = IRB.conf[:SAVE_HISTORY] if IRB.conf[:SAVE_HISTORY]
-
@extra_doc_dirs = IRB.conf[:EXTRA_DOC_DIRS]
@echo = IRB.conf[:ECHO]
@@ -140,23 +146,114 @@ module IRB
if @newline_before_multiline_output.nil?
@newline_before_multiline_output = true
end
+
+ @user_aliases = IRB.conf[:COMMAND_ALIASES].dup
+ @command_aliases = @user_aliases.merge(KEYWORD_ALIASES)
+ end
+
+ # because all input will eventually be evaluated as Ruby code,
+ # command names that conflict with Ruby keywords need special workaround
+ # we can remove them once we implemented a better command system for IRB
+ KEYWORD_ALIASES = {
+ :break => :irb_break,
+ :catch => :irb_catch,
+ :next => :irb_next,
+ }.freeze
+
+ private_constant :KEYWORD_ALIASES
+
+ def use_tracer=(val)
+ require_relative "ext/tracer" if val
+ IRB.conf[:USE_TRACER] = val
+ end
+
+ def eval_history=(val)
+ self.class.remove_method(__method__)
+ require_relative "ext/eval_history"
+ __send__(__method__, val)
+ end
+
+ def use_loader=(val)
+ self.class.remove_method(__method__)
+ require_relative "ext/use-loader"
+ __send__(__method__, val)
+ end
+
+ private def build_completor
+ completor_type = IRB.conf[:COMPLETOR]
+ case completor_type
+ when :regexp
+ return RegexpCompletor.new
+ when :type
+ completor = build_type_completor
+ return completor if completor
+ else
+ warn "Invalid value for IRB.conf[:COMPLETOR]: #{completor_type}"
+ end
+ # Fallback to RegexpCompletor
+ RegexpCompletor.new
+ end
+
+ private def build_type_completor
+ if RUBY_ENGINE == 'truffleruby'
+ # Avoid SyntaxError. truffleruby does not support endless method definition yet.
+ warn 'TypeCompletor is not supported on TruffleRuby yet'
+ return
+ end
+
+ begin
+ require 'repl_type_completor'
+ rescue LoadError => e
+ warn "TypeCompletor requires `gem repl_type_completor`: #{e.message}"
+ return
+ end
+
+ ReplTypeCompletor.preload_rbs
+ TypeCompletor.new(self)
+ end
+
+ def save_history=(val)
+ IRB.conf[:SAVE_HISTORY] = val
+ end
+
+ def save_history
+ IRB.conf[:SAVE_HISTORY]
+ end
+
+ # A copy of the default <code>IRB.conf[:HISTORY_FILE]</code>
+ def history_file
+ IRB.conf[:HISTORY_FILE]
+ end
+
+ # Set <code>IRB.conf[:HISTORY_FILE]</code> to the given +hist+.
+ def history_file=(hist)
+ IRB.conf[:HISTORY_FILE] = hist
+ end
+
+ # Workspace in the current context.
+ def workspace
+ @workspace_stack.last
+ end
+
+ # Replace the current workspace with the given +workspace+.
+ def replace_workspace(workspace)
+ @workspace_stack.pop
+ @workspace_stack.push(workspace)
end
# The top-level workspace, see WorkSpace#main
def main
- @workspace.main
+ workspace.main
end
# The toplevel workspace, see #home_workspace
attr_reader :workspace_home
- # WorkSpace in the current context.
- attr_accessor :workspace
# The current thread in this context.
attr_reader :thread
# The current input method.
#
# Can be either StdioInputMethod, ReadlineInputMethod,
- # ReidlineInputMethod, FileInputMethod or other specified when the
+ # RelineInputMethod, FileInputMethod or other specified when the
# context is created. See ::new for more # information on +input_method+.
attr_accessor :io
@@ -171,9 +268,27 @@ module IRB
# Can be either name from <code>IRB.conf[:IRB_NAME]</code>, or the number of
# the current job set by JobManager, such as <code>irb#2</code>
attr_accessor :irb_name
- # Can be either the #irb_name surrounded by parenthesis, or the
- # +input_method+ passed to Context.new
- attr_accessor :irb_path
+
+ # Can be one of the following:
+ # - the #irb_name surrounded by parenthesis
+ # - the +input_method+ passed to Context.new
+ # - the file path of the current IRB context in a binding.irb session
+ attr_reader :irb_path
+
+ # Sets @irb_path to the given +path+ as well as @eval_path
+ # @eval_path is used for evaluating code in the context of IRB session
+ # It's the same as irb_path, but with the IRB name postfix
+ # This makes sure users can distinguish the methods defined in the IRB session
+ # from the methods defined in the current file's context, especially with binding.irb
+ def irb_path=(path)
+ @irb_path = path
+
+ if File.exist?(path)
+ @eval_path = "#{path}(#{IRB.conf[:IRB_NAME]})"
+ else
+ @eval_path = path
+ end
+ end
# Whether multiline editor mode is enabled or not.
#
@@ -194,18 +309,29 @@ module IRB
attr_reader :prompt_mode
# Standard IRB prompt.
#
- # See IRB@Customizing+the+IRB+Prompt for more information.
+ # See {Custom Prompts}[rdoc-ref:IRB@Custom+Prompts] for more information.
attr_accessor :prompt_i
# IRB prompt for continuated strings.
#
- # See IRB@Customizing+the+IRB+Prompt for more information.
+ # See {Custom Prompts}[rdoc-ref:IRB@Custom+Prompts] for more information.
attr_accessor :prompt_s
# IRB prompt for continuated statement. (e.g. immediately after an +if+)
#
- # See IRB@Customizing+the+IRB+Prompt for more information.
+ # See {Custom Prompts}[rdoc-ref:IRB@Custom+Prompts] for more information.
attr_accessor :prompt_c
- # See IRB@Customizing+the+IRB+Prompt for more information.
- attr_accessor :prompt_n
+
+ # TODO: Remove this when developing v2.0
+ def prompt_n
+ warn "IRB::Context#prompt_n is deprecated and will be removed in the next major release."
+ ""
+ end
+
+ # TODO: Remove this when developing v2.0
+ def prompt_n=(_)
+ warn "IRB::Context#prompt_n= is deprecated and will be removed in the next major release."
+ ""
+ end
+
# Can be either the default <code>IRB.conf[:AUTO_INDENT]</code>, or the
# mode set by #prompt_mode=
#
@@ -313,18 +439,21 @@ module IRB
# The default value is 16.
#
# Can also be set using the +--back-trace-limit+ command line option.
- #
- # See IRB@Command+line+options for more command line options.
attr_accessor :back_trace_limit
+ # User-defined IRB command aliases
+ attr_accessor :command_aliases
+
+ attr_accessor :with_debugger
+
# Alias for #use_multiline
alias use_multiline? use_multiline
# Alias for #use_singleline
alias use_singleline? use_singleline
# backward compatibility
- alias use_reidline use_multiline
+ alias use_reline use_multiline
# backward compatibility
- alias use_reidline? use_multiline
+ alias use_reline? use_multiline
# backward compatibility
alias use_readline use_singleline
# backward compatibility
@@ -342,7 +471,7 @@ module IRB
# Returns whether messages are displayed or not.
def verbose?
if @verbose.nil?
- if @io.kind_of?(ReidlineInputMethod)
+ if @io.kind_of?(RelineInputMethod)
false
elsif defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod)
false
@@ -357,12 +486,10 @@ module IRB
end
# Whether #verbose? is +true+, and +input_method+ is either
- # StdioInputMethod or ReidlineInputMethod or ReadlineInputMethod, see #io
+ # StdioInputMethod or RelineInputMethod or ReadlineInputMethod, see #io
# for more information.
def prompting?
- verbose? || (STDIN.tty? && @io.kind_of?(StdioInputMethod) ||
- @io.kind_of?(ReidlineInputMethod) ||
- (defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod)))
+ verbose? || @io.prompting?
end
# The return value of the last statement evaluated.
@@ -372,19 +499,18 @@ module IRB
# to #last_value.
def set_last_value(value)
@last_value = value
- @workspace.local_variable_set :_, value
+ workspace.local_variable_set :_, value
end
# Sets the +mode+ of the prompt in this context.
#
- # See IRB@Customizing+the+IRB+Prompt for more information.
+ # See {Custom Prompts}[rdoc-ref:IRB@Custom+Prompts] for more information.
def prompt_mode=(mode)
@prompt_mode = mode
pconf = IRB.conf[:PROMPT][mode]
@prompt_i = pconf[:PROMPT_I]
@prompt_s = pconf[:PROMPT_S]
@prompt_c = pconf[:PROMPT_C]
- @prompt_n = pconf[:PROMPT_N]
@return_format = pconf[:RETURN]
@return_format = "%s\n" if @return_format == nil
if ai = pconf.include?(:AUTO_INDENT)
@@ -416,8 +542,6 @@ module IRB
#
# Can also be set using the +--inspect+ and +--noinspect+ command line
# options.
- #
- # See IRB@Command+line+options for more command line options.
def inspect_mode=(opt)
if i = Inspector::INSPECTORS[opt]
@@ -461,26 +585,49 @@ module IRB
@inspect_mode
end
- def evaluate(line, line_no, exception: nil) # :nodoc:
+ def evaluate(statement, line_no) # :nodoc:
@line_no = line_no
- if exception
- line_no -= 1
- line = "begin ::Kernel.raise _; rescue _.class\n#{line}\n""end"
- @workspace.local_variable_set(:_, exception)
+
+ 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)
+ set_last_value(nil)
end
- set_last_value(@workspace.evaluate(self, line, irb_path, line_no))
+
+ nil
end
- def inspect_last_value # :nodoc:
- @inspect_method.inspect_value(@last_value)
+ def evaluate_expression(code, line_no) # :nodoc:
+ result = nil
+ if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty?
+ IRB.set_measure_callback
+ end
+
+ if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty?
+ last_proc = proc do
+ result = workspace.evaluate(code, @eval_path, line_no)
+ end
+ IRB.conf[:MEASURE_CALLBACKS].inject(last_proc) do |chain, item|
+ _name, callback, arg = item
+ proc do
+ callback.(self, code, line_no, arg) do
+ chain.call
+ end
+ end
+ end.call
+ else
+ result = workspace.evaluate(code, @eval_path, line_no)
+ end
+ result
end
- alias __exit__ exit
- # Exits the current session, see IRB.irb_exit
- def exit(ret = 0)
- IRB.irb_exit(@irb, ret)
- rescue UncaughtThrowError
- super
+ def inspect_last_value # :nodoc:
+ @inspect_method.inspect_value(@last_value)
end
NOPRINTING_IVARS = ["@last_value"] # :nodoc:
@@ -509,5 +656,9 @@ module IRB
end
alias __to_s__ to_s
alias to_s inspect
+
+ def local_variables # :nodoc:
+ workspace.binding.local_variables
+ end
end
end
diff --git a/lib/irb/debug.rb b/lib/irb/debug.rb
new file mode 100644
index 0000000000..1ec2335a8e
--- /dev/null
+++ b/lib/irb/debug.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+module IRB
+ module Debug
+ IRB_DIR = File.expand_path('..', __dir__)
+
+ class << self
+ def insert_debug_break(pre_cmds: nil, do_cmds: nil)
+ options = { oneshot: true, hook_call: false }
+
+ if pre_cmds || do_cmds
+ options[:command] = ['irb', pre_cmds, do_cmds]
+ end
+ if DEBUGGER__::LineBreakpoint.instance_method(:initialize).parameters.include?([:key, :skip_src])
+ options[:skip_src] = true
+ end
+
+ # To make debugger commands like `next` or `continue` work without asking
+ # the user to quit IRB after that, we need to exit IRB first and then hit
+ # a TracePoint on #debug_break.
+ file, lineno = IRB::Irb.instance_method(:debug_break).source_location
+ DEBUGGER__::SESSION.add_line_breakpoint(file, lineno + 1, **options)
+ end
+
+ def setup(irb)
+ # When debug session is not started at all
+ unless defined?(DEBUGGER__::SESSION)
+ begin
+ require "debug/session"
+ rescue LoadError # debug.gem is not written in Gemfile
+ return false unless load_bundled_debug_gem
+ end
+ DEBUGGER__::CONFIG.set_config
+ configure_irb_for_debugger(irb)
+
+ DEBUGGER__.initialize_session{ IRB::Debug::UI.new(irb) }
+ end
+
+ # When debug session was previously started but not by IRB
+ if defined?(DEBUGGER__::SESSION) && !irb.context.with_debugger
+ configure_irb_for_debugger(irb)
+ DEBUGGER__::SESSION.reset_ui(IRB::Debug::UI.new(irb))
+ end
+
+ # Apply patches to debug gem so it skips IRB frames
+ unless DEBUGGER__.respond_to?(:capture_frames_without_irb)
+ DEBUGGER__.singleton_class.send(:alias_method, :capture_frames_without_irb, :capture_frames)
+
+ def DEBUGGER__.capture_frames(*args)
+ frames = capture_frames_without_irb(*args)
+ frames.reject! do |frame|
+ frame.realpath&.start_with?(IRB_DIR) || frame.path == "<internal:prelude>"
+ end
+ frames
+ end
+
+ DEBUGGER__::ThreadClient.prepend(SkipPathHelperForIRB)
+ end
+
+ if !@output_modifier_defined && !DEBUGGER__::CONFIG[:no_hint]
+ irb_output_modifier_proc = Reline.output_modifier_proc
+
+ Reline.output_modifier_proc = proc do |output, complete:|
+ unless output.strip.empty?
+ cmd = output.split(/\s/, 2).first
+
+ if !complete && DEBUGGER__.commands.key?(cmd)
+ output = output.sub(/\n$/, " # debug command\n")
+ end
+ end
+
+ irb_output_modifier_proc.call(output, complete: complete)
+ end
+
+ @output_modifier_defined = true
+ end
+
+ true
+ end
+
+ private
+
+ def configure_irb_for_debugger(irb)
+ require 'irb/debug/ui'
+ IRB.instance_variable_set(:@debugger_irb, irb)
+ irb.context.with_debugger = true
+ irb.context.irb_name += ":rdbg"
+ end
+
+ module SkipPathHelperForIRB
+ def skip_internal_path?(path)
+ # The latter can be removed once https://github.com/ruby/debug/issues/866 is resolved
+ super || path.match?(IRB_DIR) || path.match?('<internal:prelude>')
+ end
+ end
+
+ # This is used when debug.gem is not written in Gemfile. Even if it's not
+ # installed by `bundle install`, debug.gem is installed by default because
+ # it's a bundled gem. This method tries to activate and load that.
+ def load_bundled_debug_gem
+ # Discover latest debug.gem under GEM_PATH
+ debug_gem = Gem.paths.path.flat_map { |path| Dir.glob("#{path}/gems/debug-*") }.select do |path|
+ File.basename(path).match?(/\Adebug-\d+\.\d+\.\d+(\w+)?\z/)
+ end.sort_by do |path|
+ Gem::Version.new(File.basename(path).delete_prefix('debug-'))
+ end.last
+ return false unless debug_gem
+
+ # Discover debug/debug.so under extensions for Ruby 3.2+
+ ext_name = "/debug/debug.#{RbConfig::CONFIG['DLEXT']}"
+ ext_path = Gem.paths.path.flat_map do |path|
+ Dir.glob("#{path}/extensions/**/#{File.basename(debug_gem)}#{ext_name}")
+ end.first
+
+ # Attempt to forcibly load the bundled gem
+ if ext_path
+ $LOAD_PATH << ext_path.delete_suffix(ext_name)
+ end
+ $LOAD_PATH << "#{debug_gem}/lib"
+ begin
+ require "debug/session"
+ puts "Loaded #{File.basename(debug_gem)}"
+ true
+ rescue LoadError
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/irb/debug/ui.rb b/lib/irb/debug/ui.rb
new file mode 100644
index 0000000000..307097b8c9
--- /dev/null
+++ b/lib/irb/debug/ui.rb
@@ -0,0 +1,103 @@
+require 'io/console/size'
+require 'debug/console'
+
+module IRB
+ module Debug
+ class UI < DEBUGGER__::UI_Base
+ def initialize(irb)
+ @irb = irb
+ end
+
+ def remote?
+ false
+ end
+
+ def activate session, on_fork: false
+ end
+
+ def deactivate
+ end
+
+ def width
+ if (w = IO.console_size[1]) == 0 # for tests PTY
+ 80
+ else
+ w
+ end
+ end
+
+ def quit n
+ yield
+ exit n
+ end
+
+ def ask prompt
+ setup_interrupt do
+ print prompt
+ ($stdin.gets || '').strip
+ end
+ end
+
+ def puts str = nil
+ case str
+ when Array
+ str.each{|line|
+ $stdout.puts line.chomp
+ }
+ when String
+ str.each_line{|line|
+ $stdout.puts line.chomp
+ }
+ when nil
+ $stdout.puts
+ end
+ end
+
+ def readline _
+ setup_interrupt do
+ tc = DEBUGGER__::SESSION.instance_variable_get(:@tc)
+ cmd = @irb.debug_readline(tc.current_frame.binding || TOPLEVEL_BINDING)
+
+ case cmd
+ when nil # when user types C-d
+ "continue"
+ else
+ cmd
+ end
+ end
+ end
+
+ def setup_interrupt
+ DEBUGGER__::SESSION.intercept_trap_sigint false do
+ current_thread = Thread.current # should be session_server thread
+
+ prev_handler = trap(:INT){
+ current_thread.raise Interrupt
+ }
+
+ yield
+ ensure
+ trap(:INT, prev_handler)
+ end
+ end
+
+ def after_fork_parent
+ parent_pid = Process.pid
+
+ at_exit{
+ DEBUGGER__::SESSION.intercept_trap_sigint_end
+ trap(:SIGINT, :IGNORE)
+
+ if Process.pid == parent_pid
+ # only check child process from its parent
+ begin
+ # wait for all child processes to keep terminal
+ Process.waitpid
+ rescue Errno::ESRCH, Errno::ECHILD
+ end
+ end
+ }
+ end
+ end
+ end
+end
diff --git a/lib/irb/default_commands.rb b/lib/irb/default_commands.rb
new file mode 100644
index 0000000000..1bbc68efa7
--- /dev/null
+++ b/lib/irb/default_commands.rb
@@ -0,0 +1,260 @@
+# frozen_string_literal: true
+
+require_relative "command"
+require_relative "command/internal_helpers"
+require_relative "command/context"
+require_relative "command/exit"
+require_relative "command/force_exit"
+require_relative "command/chws"
+require_relative "command/pushws"
+require_relative "command/subirb"
+require_relative "command/load"
+require_relative "command/debug"
+require_relative "command/edit"
+require_relative "command/break"
+require_relative "command/catch"
+require_relative "command/next"
+require_relative "command/delete"
+require_relative "command/step"
+require_relative "command/continue"
+require_relative "command/finish"
+require_relative "command/backtrace"
+require_relative "command/info"
+require_relative "command/help"
+require_relative "command/show_doc"
+require_relative "command/irb_info"
+require_relative "command/ls"
+require_relative "command/measure"
+require_relative "command/show_source"
+require_relative "command/whereami"
+require_relative "command/history"
+
+module IRB
+ module Command
+ NO_OVERRIDE = 0
+ OVERRIDE_PRIVATE_ONLY = 0x01
+ OVERRIDE_ALL = 0x02
+
+ class << self
+ # This API is for IRB's internal use only and may change at any time.
+ # Please do NOT use it.
+ def _register_with_aliases(name, command_class, *aliases)
+ @commands[name.to_sym] = [command_class, aliases]
+ end
+
+ def all_commands_info
+ user_aliases = IRB.CurrentContext.command_aliases.each_with_object({}) do |(alias_name, target), result|
+ result[target] ||= []
+ result[target] << alias_name
+ end
+
+ commands.map do |command_name, (command_class, aliases)|
+ aliases = aliases.map { |a| a.first }
+
+ if additional_aliases = user_aliases[command_name]
+ aliases += additional_aliases
+ end
+
+ display_name = aliases.shift || command_name
+ {
+ display_name: display_name,
+ description: command_class.description,
+ category: command_class.category
+ }
+ end
+ end
+
+ def command_override_policies
+ @@command_override_policies ||= commands.flat_map do |cmd_name, (cmd_class, aliases)|
+ [[cmd_name, OVERRIDE_ALL]] + aliases
+ end.to_h
+ end
+
+ def execute_as_command?(name, public_method:, private_method:)
+ case command_override_policies[name]
+ when OVERRIDE_ALL
+ true
+ when OVERRIDE_PRIVATE_ONLY
+ !public_method
+ when NO_OVERRIDE
+ !public_method && !private_method
+ end
+ end
+
+ def command_names
+ command_override_policies.keys.map(&:to_s)
+ end
+
+ # Convert a command name to its implementation class if such command exists
+ def load_command(command)
+ command = command.to_sym
+ commands.each do |command_name, (command_class, aliases)|
+ if command_name == command || aliases.any? { |alias_name, _| alias_name == command }
+ return command_class
+ end
+ end
+ nil
+ end
+ end
+
+ _register_with_aliases(:irb_context, Command::Context,
+ [:context, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_exit, Command::Exit,
+ [:exit, OVERRIDE_PRIVATE_ONLY],
+ [:quit, OVERRIDE_PRIVATE_ONLY],
+ [:irb_quit, OVERRIDE_PRIVATE_ONLY]
+ )
+
+ _register_with_aliases(:irb_exit!, Command::ForceExit,
+ [:exit!, OVERRIDE_PRIVATE_ONLY]
+ )
+
+ _register_with_aliases(:irb_current_working_workspace, Command::CurrentWorkingWorkspace,
+ [:cwws, NO_OVERRIDE],
+ [:pwws, NO_OVERRIDE],
+ [:irb_print_working_workspace, OVERRIDE_ALL],
+ [:irb_cwws, OVERRIDE_ALL],
+ [:irb_pwws, OVERRIDE_ALL],
+ [:irb_current_working_binding, OVERRIDE_ALL],
+ [:irb_print_working_binding, OVERRIDE_ALL],
+ [:irb_cwb, OVERRIDE_ALL],
+ [:irb_pwb, OVERRIDE_ALL],
+ )
+
+ _register_with_aliases(:irb_change_workspace, Command::ChangeWorkspace,
+ [:chws, NO_OVERRIDE],
+ [:cws, NO_OVERRIDE],
+ [:irb_chws, OVERRIDE_ALL],
+ [:irb_cws, OVERRIDE_ALL],
+ [:irb_change_binding, OVERRIDE_ALL],
+ [:irb_cb, OVERRIDE_ALL],
+ [:cb, NO_OVERRIDE],
+ )
+
+ _register_with_aliases(:irb_workspaces, Command::Workspaces,
+ [:workspaces, NO_OVERRIDE],
+ [:irb_bindings, OVERRIDE_ALL],
+ [:bindings, NO_OVERRIDE],
+ )
+
+ _register_with_aliases(:irb_push_workspace, Command::PushWorkspace,
+ [:pushws, NO_OVERRIDE],
+ [:irb_pushws, OVERRIDE_ALL],
+ [:irb_push_binding, OVERRIDE_ALL],
+ [:irb_pushb, OVERRIDE_ALL],
+ [:pushb, NO_OVERRIDE],
+ )
+
+ _register_with_aliases(:irb_pop_workspace, Command::PopWorkspace,
+ [:popws, NO_OVERRIDE],
+ [:irb_popws, OVERRIDE_ALL],
+ [:irb_pop_binding, OVERRIDE_ALL],
+ [:irb_popb, OVERRIDE_ALL],
+ [:popb, NO_OVERRIDE],
+ )
+
+ _register_with_aliases(:irb_load, Command::Load)
+ _register_with_aliases(:irb_require, Command::Require)
+ _register_with_aliases(:irb_source, Command::Source,
+ [:source, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb, Command::IrbCommand)
+ _register_with_aliases(:irb_jobs, Command::Jobs,
+ [:jobs, NO_OVERRIDE]
+ )
+ _register_with_aliases(:irb_fg, Command::Foreground,
+ [:fg, NO_OVERRIDE]
+ )
+ _register_with_aliases(:irb_kill, Command::Kill,
+ [:kill, OVERRIDE_PRIVATE_ONLY]
+ )
+
+ _register_with_aliases(:irb_debug, Command::Debug,
+ [:debug, NO_OVERRIDE]
+ )
+ _register_with_aliases(:irb_edit, Command::Edit,
+ [:edit, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_break, Command::Break)
+ _register_with_aliases(:irb_catch, Command::Catch)
+ _register_with_aliases(:irb_next, Command::Next)
+ _register_with_aliases(:irb_delete, Command::Delete,
+ [:delete, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_step, Command::Step,
+ [:step, NO_OVERRIDE]
+ )
+ _register_with_aliases(:irb_continue, Command::Continue,
+ [:continue, NO_OVERRIDE]
+ )
+ _register_with_aliases(:irb_finish, Command::Finish,
+ [:finish, NO_OVERRIDE]
+ )
+ _register_with_aliases(:irb_backtrace, Command::Backtrace,
+ [:backtrace, NO_OVERRIDE],
+ [:bt, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_debug_info, Command::Info,
+ [:info, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_help, Command::Help,
+ [:help, NO_OVERRIDE],
+ [:show_cmds, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_show_doc, Command::ShowDoc,
+ [:show_doc, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_info, Command::IrbInfo)
+
+ _register_with_aliases(:irb_ls, Command::Ls,
+ [:ls, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_measure, Command::Measure,
+ [:measure, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_show_source, Command::ShowSource,
+ [:show_source, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_whereami, Command::Whereami,
+ [:whereami, NO_OVERRIDE]
+ )
+
+ _register_with_aliases(:irb_history, Command::History,
+ [:history, NO_OVERRIDE],
+ [:hist, NO_OVERRIDE]
+ )
+ end
+
+ ExtendCommand = Command
+
+ # For backward compatibility, we need to keep this module:
+ # - As a container of helper methods
+ # - As a place to register commands with the deprecated def_extend_command method
+ module ExtendCommandBundle
+ # For backward compatibility
+ NO_OVERRIDE = Command::NO_OVERRIDE
+ OVERRIDE_PRIVATE_ONLY = Command::OVERRIDE_PRIVATE_ONLY
+ OVERRIDE_ALL = Command::OVERRIDE_ALL
+
+ # Deprecated. Doesn't have any effect.
+ @EXTEND_COMMANDS = []
+
+ # Drepcated. Use Command.regiser instead.
+ def self.def_extend_command(cmd_name, cmd_class, _, *aliases)
+ Command._register_with_aliases(cmd_name, cmd_class, *aliases)
+ Command.class_variable_set(:@@command_override_policies, nil)
+ end
+ end
+end
diff --git a/lib/irb/easter-egg.rb b/lib/irb/easter-egg.rb
index 3e79692de9..adc0834d55 100644
--- a/lib/irb/easter-egg.rb
+++ b/lib/irb/easter-egg.rb
@@ -98,18 +98,26 @@ module IRB
end
end
+ private def easter_egg_logo(type)
+ @easter_egg_logos ||= File.read(File.join(__dir__, 'ruby_logo.aa'), encoding: 'UTF-8:UTF-8')
+ .split(/TYPE: ([A-Z]+)\n/)[1..]
+ .each_slice(2)
+ .to_h
+ @easter_egg_logos[type.to_s.upcase]
+ end
+
private def easter_egg(type = nil)
type ||= [:logo, :dancing].sample
case type
when :logo
- File.open(File.join(__dir__, 'ruby_logo.aa')) do |f|
- require "rdoc"
- RDoc::RI::Driver.new.page do |io|
- IO.copy_stream(f, io)
- end
+ require "rdoc"
+ RDoc::RI::Driver.new.page do |io|
+ io.write easter_egg_logo(:large)
end
when :dancing
- begin
+ STDOUT.cooked do
+ interrupted = false
+ prev_trap = trap("SIGINT") { interrupted = true }
canvas = Canvas.new(Reline.get_screen_size)
Reline::IOGate.set_winch_handler do
canvas = Canvas.new(Reline.get_screen_size)
@@ -125,10 +133,12 @@ module IRB
buff[0, 20] = "\e[0mPress Ctrl+C to stop\e[31m\e[1m"
print "\e[H" + buff
sleep 0.05
+ break if interrupted
end
rescue Interrupt
ensure
print "\e[0m\e[?1049l"
+ trap("SIGINT", prev_trap)
end
end
end
diff --git a/lib/irb/ext/change-ws.rb b/lib/irb/ext/change-ws.rb
index 4c57e44eab..60e8afe31f 100644
--- a/lib/irb/ext/change-ws.rb
+++ b/lib/irb/ext/change-ws.rb
@@ -1,14 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/ext/cb.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB # :nodoc:
class Context
@@ -18,7 +12,7 @@ module IRB # :nodoc:
if defined? @home_workspace
@home_workspace
else
- @home_workspace = @workspace
+ @home_workspace = workspace
end
end
@@ -31,15 +25,13 @@ module IRB # :nodoc:
# See IRB::WorkSpace.new for more information.
def change_workspace(*_main)
if _main.empty?
- @workspace = home_workspace
+ replace_workspace(home_workspace)
return main
end
- @workspace = WorkSpace.new(_main[0])
-
- if !(class<<main;ancestors;end).include?(ExtendCommandBundle)
- main.extend ExtendCommandBundle
- end
+ workspace = WorkSpace.new(_main[0])
+ replace_workspace(workspace)
+ workspace.load_helper_methods_to_main
end
end
end
diff --git a/lib/irb/ext/history.rb b/lib/irb/ext/eval_history.rb
index fc304c6f6c..6c21ff00ee 100644
--- a/lib/irb/ext/history.rb
+++ b/lib/irb/ext/eval_history.rb
@@ -1,14 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# history.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB # :nodoc:
@@ -24,7 +18,7 @@ module IRB # :nodoc:
if defined?(@eval_history) && @eval_history
@eval_history_values.push @line_no, @last_value
- @workspace.evaluate self, "__ = IRB.CurrentContext.instance_eval{@eval_history_values}"
+ workspace.evaluate "__ = IRB.CurrentContext.instance_eval{@eval_history_values}"
end
@last_value
@@ -46,16 +40,16 @@ module IRB # :nodoc:
#
# If +no+ is +nil+, execution result history isn't used (default).
#
- # History values are available via <code>__</code> variable, see
- # IRB::History.
+ # EvalHistory values are available via <code>__</code> variable, see
+ # IRB::EvalHistory.
def eval_history=(no)
if no
if defined?(@eval_history) && @eval_history
@eval_history_values.size(no)
else
- @eval_history_values = History.new(no)
+ @eval_history_values = EvalHistory.new(no)
IRB.conf[:__TMP__EHV__] = @eval_history_values
- @workspace.evaluate(self, "__ = IRB.conf[:__TMP__EHV__]")
+ workspace.evaluate("__ = IRB.conf[:__TMP__EHV__]")
IRB.conf.delete(:__TMP_EHV__)
end
else
@@ -95,7 +89,7 @@ module IRB # :nodoc:
# __[1]
# # => 10
#
- class History
+ class EvalHistory
def initialize(size = 16) # :nodoc:
@size = size
diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb
index af028996e7..df5aaa8e5a 100644
--- a/lib/irb/ext/loader.rb
+++ b/lib/irb/ext/loader.rb
@@ -1,15 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# loader.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
-
module IRB # :nodoc:
# Raised in the event of an exception in a file loaded from an Irb session
@@ -31,31 +24,8 @@ module IRB # :nodoc:
load_file(path, priv)
end
- if File.respond_to?(:absolute_path?)
- def absolute_path?(path)
- File.absolute_path?(path)
- end
- else
- separator =
- if File::ALT_SEPARATOR
- "[#{Regexp.quote(File::SEPARATOR + File::ALT_SEPARATOR)}]"
- else
- File::SEPARATOR
- end
- ABSOLUTE_PATH_PATTERN = # :nodoc:
- case Dir.pwd
- when /\A\w:/, /\A#{separator}{2}/
- /\A(?:\w:|#{separator})#{separator}/
- else
- /\A#{separator}/
- end
- def absolute_path?(path)
- ABSOLUTE_PATH_PATTERN =~ path
- end
- end
-
def search_file_from_ruby_path(fn) # :nodoc:
- if absolute_path?(fn)
+ if File.absolute_path?(fn)
return fn if File.exist?(fn)
return nil
end
@@ -72,6 +42,7 @@ module IRB # :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
@@ -96,6 +67,7 @@ module IRB # :nodoc:
#
# 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
@@ -126,13 +98,13 @@ module IRB # :nodoc:
def old # :nodoc:
back_io = @io
- back_path = @irb_path
+ back_path = irb_path
back_name = @irb_name
back_scanner = @irb.scanner
begin
@io = FileInputMethod.new(path)
@irb_name = File.basename(path)
- @irb_path = path
+ self.irb_path = path
@irb.signal_status(:IN_LOAD) do
if back_io.kind_of?(FileInputMethod)
@irb.eval_input
@@ -147,7 +119,7 @@ module IRB # :nodoc:
ensure
@io = back_io
@irb_name = back_name
- @irb_path = back_path
+ self.irb_path = back_path
@irb.scanner = back_scanner
end
end
diff --git a/lib/irb/ext/multi-irb.rb b/lib/irb/ext/multi-irb.rb
index 74de1ecde5..9f234f0cdc 100644
--- a/lib/irb/ext/multi-irb.rb
+++ b/lib/irb/ext/multi-irb.rb
@@ -1,18 +1,11 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/multi-irb.rb - multiple irb module
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
-fail CantShiftToMultiIrbMode unless defined?(Thread)
module IRB
- class JobManager
+ class JobManager # :nodoc:
# Creates a new JobManager object
def initialize
@@ -173,12 +166,12 @@ module IRB
@JobManager = JobManager.new
# The current JobManager in the session
- def IRB.JobManager
+ def IRB.JobManager # :nodoc:
@JobManager
end
# The current Context in this session
- def IRB.CurrentContext
+ def IRB.CurrentContext # :nodoc:
IRB.JobManager.irb(Thread.current).context
end
@@ -186,7 +179,7 @@ module IRB
#
# The optional +file+ argument is given to Context.new, along with the
# workspace created with the remaining arguments, see WorkSpace.new
- def IRB.irb(file = nil, *main)
+ def IRB.irb(file = nil, *main) # :nodoc:
workspace = WorkSpace.new(*main)
parent_thread = Thread.current
Thread.start do
diff --git a/lib/irb/ext/save-history.rb b/lib/irb/ext/save-history.rb
deleted file mode 100644
index 7acaebe36a..0000000000
--- a/lib/irb/ext/save-history.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: false
-# save-history.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-
-module IRB
- module HistorySavingAbility # :nodoc:
- end
-
- class Context
- def init_save_history# :nodoc:
- unless (class<<@io;self;end).include?(HistorySavingAbility)
- @io.extend(HistorySavingAbility)
- end
- end
-
- # A copy of the default <code>IRB.conf[:SAVE_HISTORY]</code>
- def save_history
- IRB.conf[:SAVE_HISTORY]
- end
-
- remove_method(:save_history=) if method_defined?(:save_history=)
- # Sets <code>IRB.conf[:SAVE_HISTORY]</code> to the given +val+ and calls
- # #init_save_history with this context.
- #
- # Will store the number of +val+ entries of history in the #history_file
- #
- # Add the following to your +.irbrc+ to change the number of history
- # entries stored to 1000:
- #
- # IRB.conf[:SAVE_HISTORY] = 1000
- def save_history=(val)
- IRB.conf[:SAVE_HISTORY] = val
- if val
- main_context = IRB.conf[:MAIN_CONTEXT]
- main_context = self unless main_context
- main_context.init_save_history
- end
- end
-
- # A copy of the default <code>IRB.conf[:HISTORY_FILE]</code>
- def history_file
- IRB.conf[:HISTORY_FILE]
- end
-
- # Set <code>IRB.conf[:HISTORY_FILE]</code> to the given +hist+.
- def history_file=(hist)
- IRB.conf[:HISTORY_FILE] = hist
- end
- end
-
- module HistorySavingAbility # :nodoc:
- def HistorySavingAbility.extended(obj)
- IRB.conf[:AT_EXIT].push proc{obj.save_history}
- obj.load_history
- obj
- end
-
- def load_history
- return unless self.class.const_defined?(:HISTORY)
- history = self.class::HISTORY
- if history_file = IRB.conf[:HISTORY_FILE]
- history_file = File.expand_path(history_file)
- end
- history_file = IRB.rc_file("_history") unless history_file
- if File.exist?(history_file)
- open(history_file, "r:#{IRB.conf[:LC_MESSAGES].encoding}") do |f|
- f.each { |l|
- l = l.chomp
- if self.class == ReidlineInputMethod and history.last&.end_with?("\\")
- history.last.delete_suffix!("\\")
- history.last << "\n" << l
- else
- history << l
- end
- }
- end
- @loaded_history_lines = history.size
- @loaded_history_mtime = File.mtime(history_file)
- end
- end
-
- def save_history
- return unless self.class.const_defined?(:HISTORY)
- history = self.class::HISTORY
- if num = IRB.conf[:SAVE_HISTORY] and (num = num.to_i) != 0
- if history_file = IRB.conf[:HISTORY_FILE]
- history_file = File.expand_path(history_file)
- end
- history_file = IRB.rc_file("_history") unless history_file
-
- # Change the permission of a file that already exists[BUG #7694]
- begin
- if File.stat(history_file).mode & 066 != 0
- File.chmod(0600, history_file)
- end
- rescue Errno::ENOENT
- rescue Errno::EPERM
- return
- rescue
- raise
- end
-
- if File.exist?(history_file) && @loaded_history_mtime &&
- File.mtime(history_file) != @loaded_history_mtime
- history = history[@loaded_history_lines..-1]
- append_history = true
- end
-
- open(history_file, "#{append_history ? 'a' : 'w'}:#{IRB.conf[:LC_MESSAGES].encoding}", 0600) do |f|
- hist = history.map{ |l| l.split("\n").join("\\\n") }
- unless append_history
- begin
- hist = hist.last(num) if hist.size > num and num > 0
- rescue RangeError # bignum too big to convert into `long'
- # Do nothing because the bignum should be treated as inifinity
- end
- end
- f.puts(hist)
- end
- end
- end
- end
-end
diff --git a/lib/irb/ext/tracer.rb b/lib/irb/ext/tracer.rb
index 67ac4bb965..fd6daa88ae 100644
--- a/lib/irb/ext/tracer.rb
+++ b/lib/irb/ext/tracer.rb
@@ -1,84 +1,39 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/lib/tracer.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
+# Loading the gem "tracer" will cause it to extend IRB commands with:
+# https://github.com/ruby/tracer/blob/v0.2.2/lib/tracer/irb.rb
begin
require "tracer"
rescue LoadError
- $stderr.puts "Tracer extension of IRB is enabled but tracer gem doesn't found."
- module IRB
- TracerLoadError = true
- class Context
- def use_tracer=(opt)
- # do nothing
- end
- end
- end
+ $stderr.puts "Tracer extension of IRB is enabled but tracer gem wasn't found."
return # This is about to disable loading below
end
module IRB
+ class CallTracer < ::CallTracer
+ IRB_DIR = File.expand_path('../..', __dir__)
- # initialize tracing function
- def IRB.initialize_tracer
- Tracer.verbose = false
- Tracer.add_filter {
- |event, file, line, id, binding, *rests|
- /^#{Regexp.quote(@CONF[:IRB_LIB_PATH])}/ !~ file and
- File::basename(file) != "irb.rb"
- }
- end
-
- class Context
- # Whether Tracer is used when evaluating statements in this context.
- #
- # See +lib/tracer.rb+ for more information.
- attr_reader :use_tracer
- alias use_tracer? use_tracer
-
- # Sets whether or not to use the Tracer library when evaluating statements
- # in this context.
- #
- # See +lib/tracer.rb+ for more information.
- def use_tracer=(opt)
- if opt
- Tracer.set_get_line_procs(@irb_path) {
- |line_no, *rests|
- @io.line(line_no)
- }
- elsif !opt && @use_tracer
- Tracer.off
- end
- @use_tracer=opt
+ def skip?(tp)
+ super || tp.path.match?(IRB_DIR) || tp.path.match?('<internal:prelude>')
end
end
-
class WorkSpace
alias __evaluate__ evaluate
# Evaluate the context of this workspace and use the Tracer library to
# output the exact lines of code are being executed in chronological order.
#
- # See +lib/tracer.rb+ for more information.
- def evaluate(context, statements, file = nil, line = nil)
- if context.use_tracer? && file != nil && line != nil
- Tracer.on
- begin
- __evaluate__(context, statements, file, line)
- ensure
- Tracer.off
+ # See https://github.com/ruby/tracer for more information.
+ def evaluate(statements, file = __FILE__, line = __LINE__)
+ if IRB.conf[:USE_TRACER] == true
+ CallTracer.new(colorize: Color.colorable?).start do
+ __evaluate__(statements, file, line)
end
else
- __evaluate__(context, statements, file || __FILE__, line || __LINE__)
+ __evaluate__(statements, file, line)
end
end
end
-
- IRB.initialize_tracer
end
diff --git a/lib/irb/ext/use-loader.rb b/lib/irb/ext/use-loader.rb
index 1897bc89e0..c8a3ea1fe8 100644
--- a/lib/irb/ext/use-loader.rb
+++ b/lib/irb/ext/use-loader.rb
@@ -1,16 +1,10 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# use-loader.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
-require_relative "../cmd/load"
+require_relative "../command/load"
require_relative "loader"
class Object
@@ -23,12 +17,12 @@ module IRB
remove_method :irb_load if method_defined?(:irb_load)
# Loads the given file similarly to Kernel#load, see IrbLoader#irb_load
def irb_load(*opts, &b)
- ExtendCommand::Load.execute(irb_context, *opts, &b)
+ Command::Load.execute(irb_context, *opts, &b)
end
remove_method :irb_require if method_defined?(:irb_require)
# Loads the given file similarly to Kernel#require
def irb_require(*opts, &b)
- ExtendCommand::Require.execute(irb_context, *opts, &b)
+ Command::Require.execute(irb_context, *opts, &b)
end
end
@@ -55,14 +49,12 @@ module IRB
if IRB.conf[:USE_LOADER] != opt
IRB.conf[:USE_LOADER] = opt
if opt
- if !$".include?("irb/cmd/load")
- end
- (class<<@workspace.main;self;end).instance_eval {
+ (class<<workspace.main;self;end).instance_eval {
alias_method :load, :irb_load
alias_method :require, :irb_require
}
else
- (class<<@workspace.main;self;end).instance_eval {
+ (class<<workspace.main;self;end).instance_eval {
alias_method :load, :__original__load__IRB_use_loader__
alias_method :require, :__original__require__IRB_use_loader__
}
diff --git a/lib/irb/ext/workspaces.rb b/lib/irb/ext/workspaces.rb
index 730b58e64d..da09faa83e 100644
--- a/lib/irb/ext/workspaces.rb
+++ b/lib/irb/ext/workspaces.rb
@@ -1,32 +1,11 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# push-ws.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB # :nodoc:
class Context
-
- # Size of the current WorkSpace stack
- def irb_level
- workspace_stack.size
- end
-
- # WorkSpaces in the current stack
- def workspaces
- if defined? @workspaces
- @workspaces
- else
- @workspaces = []
- end
- end
-
# Creates a new workspace with the given object or binding, and appends it
# onto the current #workspaces stack.
#
@@ -34,20 +13,15 @@ module IRB # :nodoc:
# information.
def push_workspace(*_main)
if _main.empty?
- if workspaces.empty?
- print "No other workspace\n"
- return nil
+ 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
- ws = workspaces.pop
- workspaces.push @workspace
- @workspace = ws
- return workspaces
- end
-
- workspaces.push @workspace
- @workspace = WorkSpace.new(@workspace.binding, _main[0])
- if !(class<<main;ancestors;end).include?(ExtendCommandBundle)
- main.extend ExtendCommandBundle
+ else
+ new_workspace = WorkSpace.new(workspace.binding, _main[0])
+ @workspace_stack.push new_workspace
+ new_workspace.load_helper_methods_to_main
end
end
@@ -56,11 +30,7 @@ module IRB # :nodoc:
#
# Also, see #push_workspace.
def pop_workspace
- if workspaces.empty?
- print "workspace stack empty\n"
- return
- end
- @workspace = workspaces.pop
+ @workspace_stack.pop if @workspace_stack.size > 1
end
end
end
diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb
deleted file mode 100644
index 7778a0d0ce..0000000000
--- a/lib/irb/extend-command.rb
+++ /dev/null
@@ -1,356 +0,0 @@
-# frozen_string_literal: false
-#
-# irb/extend-command.rb - irb extend command
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
-module IRB # :nodoc:
- # Installs the default irb extensions command bundle.
- module ExtendCommandBundle
- EXCB = ExtendCommandBundle # :nodoc:
-
- # See #install_alias_method.
- NO_OVERRIDE = 0
- # See #install_alias_method.
- OVERRIDE_PRIVATE_ONLY = 0x01
- # See #install_alias_method.
- OVERRIDE_ALL = 0x02
-
- # Quits the current irb context
- #
- # +ret+ is the optional signal or message to send to Context#exit
- #
- # Same as <code>IRB.CurrentContext.exit</code>.
- def irb_exit(ret = 0)
- irb_context.exit(ret)
- end
-
- # Displays current configuration.
- #
- # Modifying the configuration is achieved by sending a message to IRB.conf.
- def irb_context
- IRB.CurrentContext
- end
-
- @ALIASES = [
- [:context, :irb_context, NO_OVERRIDE],
- [:conf, :irb_context, NO_OVERRIDE],
- [:irb_quit, :irb_exit, OVERRIDE_PRIVATE_ONLY],
- [:exit, :irb_exit, OVERRIDE_PRIVATE_ONLY],
- [:quit, :irb_exit, OVERRIDE_PRIVATE_ONLY],
- ]
-
- @EXTEND_COMMANDS = [
- [
- :irb_current_working_workspace, :CurrentWorkingWorkspace, "cmd/chws",
- [:irb_print_working_workspace, OVERRIDE_ALL],
- [:irb_cwws, OVERRIDE_ALL],
- [:irb_pwws, OVERRIDE_ALL],
- [:cwws, NO_OVERRIDE],
- [:pwws, NO_OVERRIDE],
- [:irb_current_working_binding, OVERRIDE_ALL],
- [:irb_print_working_binding, OVERRIDE_ALL],
- [:irb_cwb, OVERRIDE_ALL],
- [:irb_pwb, OVERRIDE_ALL],
- ],
- [
- :irb_change_workspace, :ChangeWorkspace, "cmd/chws",
- [:irb_chws, OVERRIDE_ALL],
- [:irb_cws, OVERRIDE_ALL],
- [:chws, NO_OVERRIDE],
- [:cws, NO_OVERRIDE],
- [:irb_change_binding, OVERRIDE_ALL],
- [:irb_cb, OVERRIDE_ALL],
- [:cb, NO_OVERRIDE],
- ],
-
- [
- :irb_workspaces, :Workspaces, "cmd/pushws",
- [:workspaces, NO_OVERRIDE],
- [:irb_bindings, OVERRIDE_ALL],
- [:bindings, NO_OVERRIDE],
- ],
- [
- :irb_push_workspace, :PushWorkspace, "cmd/pushws",
- [:irb_pushws, OVERRIDE_ALL],
- [:pushws, NO_OVERRIDE],
- [:irb_push_binding, OVERRIDE_ALL],
- [:irb_pushb, OVERRIDE_ALL],
- [:pushb, NO_OVERRIDE],
- ],
- [
- :irb_pop_workspace, :PopWorkspace, "cmd/pushws",
- [:irb_popws, OVERRIDE_ALL],
- [:popws, NO_OVERRIDE],
- [:irb_pop_binding, OVERRIDE_ALL],
- [:irb_popb, OVERRIDE_ALL],
- [:popb, NO_OVERRIDE],
- ],
-
- [
- :irb_load, :Load, "cmd/load"],
- [
- :irb_require, :Require, "cmd/load"],
- [
- :irb_source, :Source, "cmd/load",
- [:source, NO_OVERRIDE],
- ],
-
- [
- :irb, :IrbCommand, "cmd/subirb"],
- [
- :irb_jobs, :Jobs, "cmd/subirb",
- [:jobs, NO_OVERRIDE],
- ],
- [
- :irb_fg, :Foreground, "cmd/subirb",
- [:fg, NO_OVERRIDE],
- ],
- [
- :irb_kill, :Kill, "cmd/subirb",
- [:kill, OVERRIDE_PRIVATE_ONLY],
- ],
-
- [
- :irb_help, :Help, "cmd/help",
- [:help, NO_OVERRIDE],
- ],
-
- [
- :irb_info, :Info, "cmd/info"
- ],
-
- [
- :irb_ls, :Ls, "cmd/ls",
- [:ls, NO_OVERRIDE],
- ],
-
- [
- :irb_measure, :Measure, "cmd/measure",
- [:measure, NO_OVERRIDE],
- ],
-
- [
- :irb_show_source, :ShowSource, "cmd/show_source",
- [:show_source, NO_OVERRIDE],
- ],
-
- [
- :irb_whereami, :Whereami, "cmd/whereami",
- [:whereami, NO_OVERRIDE],
- ],
-
- ]
-
- # Installs the default irb commands:
- #
- # +irb_current_working_workspace+:: Context#main
- # +irb_change_workspace+:: Context#change_workspace
- # +irb_workspaces+:: Context#workspaces
- # +irb_push_workspace+:: Context#push_workspace
- # +irb_pop_workspace+:: Context#pop_workspace
- # +irb_load+:: #irb_load
- # +irb_require+:: #irb_require
- # +irb_source+:: IrbLoader#source_file
- # +irb+:: IRB.irb
- # +irb_jobs+:: JobManager
- # +irb_fg+:: JobManager#switch
- # +irb_kill+:: JobManager#kill
- # +irb_help+:: IRB@Command+line+options
- def self.install_extend_commands
- for args in @EXTEND_COMMANDS
- def_extend_command(*args)
- end
- end
-
- # Evaluate the given +cmd_name+ on the given +cmd_class+ Class.
- #
- # Will also define any given +aliases+ for the method.
- #
- # The optional +load_file+ parameter will be required within the method
- # definition.
- def self.def_extend_command(cmd_name, cmd_class, load_file = nil, *aliases)
- case cmd_class
- when Symbol
- cmd_class = cmd_class.id2name
- when String
- when Class
- cmd_class = cmd_class.name
- end
-
- if load_file
- kwargs = ", **kwargs" if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.7.0"
- line = __LINE__; eval %[
- def #{cmd_name}(*opts#{kwargs}, &b)
- require_relative "#{load_file}"
- arity = ExtendCommand::#{cmd_class}.instance_method(:execute).arity
- args = (1..(arity < 0 ? ~arity : arity)).map {|i| "arg" + i.to_s }
- args << "*opts#{kwargs}" if arity < 0
- args << "&block"
- args = args.join(", ")
- line = __LINE__; eval %[
- unless singleton_class.class_variable_defined?(:@@#{cmd_name}_)
- singleton_class.class_variable_set(:@@#{cmd_name}_, true)
- def self.#{cmd_name}_(\#{args})
- ExtendCommand::#{cmd_class}.execute(irb_context, \#{args})
- end
- end
- ], nil, __FILE__, line
- __send__ :#{cmd_name}_, *opts#{kwargs}, &b
- end
- ], nil, __FILE__, line
- else
- line = __LINE__; eval %[
- def #{cmd_name}(*opts, &b)
- ExtendCommand::#{cmd_class}.execute(irb_context, *opts, &b)
- end
- ], nil, __FILE__, line
- end
-
- for ali, flag in aliases
- @ALIASES.push [ali, cmd_name, flag]
- end
- end
-
- # Installs alias methods for the default irb commands, see
- # ::install_extend_commands.
- def install_alias_method(to, from, override = NO_OVERRIDE)
- to = to.id2name unless to.kind_of?(String)
- from = from.id2name unless from.kind_of?(String)
-
- if override == OVERRIDE_ALL or
- (override == OVERRIDE_PRIVATE_ONLY) && !respond_to?(to) or
- (override == NO_OVERRIDE) && !respond_to?(to, true)
- target = self
- (class << self; self; end).instance_eval{
- if target.respond_to?(to, true) &&
- !target.respond_to?(EXCB.irb_original_method_name(to), true)
- alias_method(EXCB.irb_original_method_name(to), to)
- end
- alias_method to, from
- }
- else
- Kernel.print "irb: warn: can't alias #{to} from #{from}.\n"
- end
- end
-
- def self.irb_original_method_name(method_name) # :nodoc:
- "irb_" + method_name + "_org"
- end
-
- # Installs alias methods for the default irb commands on the given object
- # using #install_alias_method.
- def self.extend_object(obj)
- unless (class << obj; ancestors; end).include?(EXCB)
- super
- for ali, com, flg in @ALIASES
- obj.install_alias_method(ali, com, flg)
- end
- end
- end
-
- install_extend_commands
- end
-
- # Extends methods for the Context module
- module ContextExtender
- CE = ContextExtender # :nodoc:
-
- @EXTEND_COMMANDS = [
- [:eval_history=, "ext/history.rb"],
- [:use_tracer=, "ext/tracer.rb"],
- [:use_loader=, "ext/use-loader.rb"],
- [:save_history=, "ext/save-history.rb"],
- ]
-
- # Installs the default context extensions as irb commands:
- #
- # Context#eval_history=:: +irb/ext/history.rb+
- # Context#use_tracer=:: +irb/ext/tracer.rb+
- # Context#use_loader=:: +irb/ext/use-loader.rb+
- # Context#save_history=:: +irb/ext/save-history.rb+
- def self.install_extend_commands
- for args in @EXTEND_COMMANDS
- def_extend_command(*args)
- end
- end
-
- # Evaluate the given +command+ from the given +load_file+ on the Context
- # module.
- #
- # Will also define any given +aliases+ for the method.
- def self.def_extend_command(cmd_name, load_file, *aliases)
- line = __LINE__; Context.module_eval %[
- def #{cmd_name}(*opts, &b)
- Context.module_eval {remove_method(:#{cmd_name})}
- require_relative "#{load_file}"
- __send__ :#{cmd_name}, *opts, &b
- end
- for ali in aliases
- alias_method ali, cmd_name
- end
- ], __FILE__, line
- end
-
- CE.install_extend_commands
- end
-
- # A convenience module for extending Ruby methods.
- module MethodExtender
- # Extends the given +base_method+ with a prefix call to the given
- # +extend_method+.
- def def_pre_proc(base_method, extend_method)
- base_method = base_method.to_s
- extend_method = extend_method.to_s
-
- alias_name = new_alias_name(base_method)
- module_eval %[
- alias_method alias_name, base_method
- def #{base_method}(*opts)
- __send__ :#{extend_method}, *opts
- __send__ :#{alias_name}, *opts
- end
- ]
- end
-
- # Extends the given +base_method+ with a postfix call to the given
- # +extend_method+.
- def def_post_proc(base_method, extend_method)
- base_method = base_method.to_s
- extend_method = extend_method.to_s
-
- alias_name = new_alias_name(base_method)
- module_eval %[
- alias_method alias_name, base_method
- def #{base_method}(*opts)
- __send__ :#{alias_name}, *opts
- __send__ :#{extend_method}, *opts
- end
- ]
- end
-
- # Returns a unique method name to use as an alias for the given +name+.
- #
- # Usually returns <code>#{prefix}#{name}#{postfix}<num></code>, example:
- #
- # new_alias_name('foo') #=> __alias_of__foo__
- # def bar; end
- # new_alias_name('bar') #=> __alias_of__bar__2
- def new_alias_name(name, prefix = "__alias_of__", postfix = "__")
- base_name = "#{prefix}#{name}#{postfix}"
- all_methods = instance_methods(true) + private_instance_methods(true)
- same_methods = all_methods.grep(/^#{Regexp.quote(base_name)}[0-9]*$/)
- return base_name if same_methods.empty?
- no = same_methods.size
- while !same_methods.include?(alias_name = base_name + no)
- no += 1
- end
- alias_name
- end
- end
-end
diff --git a/lib/irb/frame.rb b/lib/irb/frame.rb
index de54a98f1b..4b697c8719 100644
--- a/lib/irb/frame.rb
+++ b/lib/irb/frame.rb
@@ -1,14 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# frame.rb -
-# $Release Version: 0.9$
-# $Revision$
# by Keiju ISHITSUKA(Nihon Rational Software Co.,Ltd)
#
-# --
-#
-#
-#
module IRB
class Frame
diff --git a/lib/irb/help.rb b/lib/irb/help.rb
index 3eeaf841b0..a24bc10a15 100644
--- a/lib/irb/help.rb
+++ b/lib/irb/help.rb
@@ -1,24 +1,16 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/help.rb - print usage module
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ishitsuka.com)
#
-# --
-#
-#
-#
-
-require_relative 'magic-file'
module IRB
- # Outputs the irb help message, see IRB@Command+line+options.
- def IRB.print_usage
+ # Outputs the irb help message, see IRB@Command-Line+Options.
+ def IRB.print_usage # :nodoc:
lc = IRB.conf[:LC_MESSAGES]
path = lc.find("irb/help-message")
space_line = false
- IRB::MagicFile.open(path){|f|
+ File.open(path){|f|
f.each_line do |l|
if /^\s*$/ =~ l
lc.puts l unless space_line
diff --git a/lib/irb/helper_method.rb b/lib/irb/helper_method.rb
new file mode 100644
index 0000000000..f1f6fff915
--- /dev/null
+++ b/lib/irb/helper_method.rb
@@ -0,0 +1,29 @@
+require_relative "helper_method/base"
+
+module IRB
+ module HelperMethod
+ @helper_methods = {}
+
+ class << self
+ attr_reader :helper_methods
+
+ def register(name, helper_class)
+ @helper_methods[name] = helper_class
+
+ if defined?(HelpersContainer)
+ HelpersContainer.install_helper_methods
+ end
+ end
+
+ def all_helper_methods_info
+ @helper_methods.map do |name, helper_class|
+ { display_name: name, description: helper_class.description }
+ end
+ end
+ end
+
+ # Default helper_methods
+ require_relative "helper_method/conf"
+ register(:conf, HelperMethod::Conf)
+ end
+end
diff --git a/lib/irb/helper_method/base.rb b/lib/irb/helper_method/base.rb
new file mode 100644
index 0000000000..a68001ed28
--- /dev/null
+++ b/lib/irb/helper_method/base.rb
@@ -0,0 +1,16 @@
+require "singleton"
+
+module IRB
+ module HelperMethod
+ class Base
+ include Singleton
+
+ class << self
+ def description(description = nil)
+ @description = description if description
+ @description
+ end
+ end
+ end
+ end
+end
diff --git a/lib/irb/helper_method/conf.rb b/lib/irb/helper_method/conf.rb
new file mode 100644
index 0000000000..460f5ab78a
--- /dev/null
+++ b/lib/irb/helper_method/conf.rb
@@ -0,0 +1,11 @@
+module IRB
+ module HelperMethod
+ class Conf < Base
+ description "Returns the current context."
+
+ def execute
+ IRB.CurrentContext
+ end
+ end
+ end
+end
diff --git a/lib/irb/history.rb b/lib/irb/history.rb
new file mode 100644
index 0000000000..685354b2d8
--- /dev/null
+++ b/lib/irb/history.rb
@@ -0,0 +1,87 @@
+require "pathname"
+
+module IRB
+ module HistorySavingAbility # :nodoc:
+ def support_history_saving?
+ true
+ end
+
+ def reset_history_counter
+ @loaded_history_lines = self.class::HISTORY.size
+ end
+
+ def load_history
+ history = self.class::HISTORY
+
+ if history_file = IRB.conf[:HISTORY_FILE]
+ history_file = File.expand_path(history_file)
+ end
+ history_file = IRB.rc_file("_history") unless history_file
+ if history_file && File.exist?(history_file)
+ File.open(history_file, "r:#{IRB.conf[:LC_MESSAGES].encoding}") do |f|
+ f.each { |l|
+ l = l.chomp
+ if self.class == RelineInputMethod and history.last&.end_with?("\\")
+ history.last.delete_suffix!("\\")
+ history.last << "\n" << l
+ else
+ history << l
+ end
+ }
+ end
+ @loaded_history_lines = history.size
+ @loaded_history_mtime = File.mtime(history_file)
+ end
+ end
+
+ def save_history
+ history = self.class::HISTORY.to_a
+
+ if num = IRB.conf[:SAVE_HISTORY] and (num = num.to_i) != 0
+ if history_file = IRB.conf[:HISTORY_FILE]
+ history_file = File.expand_path(history_file)
+ end
+ history_file = IRB.rc_file("_history") unless history_file
+
+ # When HOME and XDG_CONFIG_HOME are not available, history_file might be nil
+ return unless history_file
+
+ # Change the permission of a file that already exists[BUG #7694]
+ begin
+ if File.stat(history_file).mode & 066 != 0
+ File.chmod(0600, history_file)
+ end
+ rescue Errno::ENOENT
+ rescue Errno::EPERM
+ return
+ rescue
+ raise
+ end
+
+ if File.exist?(history_file) &&
+ File.mtime(history_file) != @loaded_history_mtime
+ history = history[@loaded_history_lines..-1] if @loaded_history_lines
+ append_history = true
+ end
+
+ pathname = Pathname.new(history_file)
+ unless Dir.exist?(pathname.dirname)
+ warn "Warning: The directory to save IRB's history file does not exist. Please double check `IRB.conf[:HISTORY_FILE]`'s value."
+ return
+ end
+
+ File.open(history_file, (append_history ? 'a' : 'w'), 0o600, encoding: IRB.conf[:LC_MESSAGES]&.encoding) do |f|
+ hist = history.map{ |l| l.scrub.split("\n").join("\\\n") }
+ unless append_history
+ begin
+ hist = hist.last(num) if hist.size > num and num > 0
+ rescue RangeError # bignum too big to convert into `long'
+ # Do nothing because the bignum should be treated as infinity
+ end
+ end
+ f.puts(hist)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/irb/init.rb b/lib/irb/init.rb
index d2baee2017..355047519c 100644
--- a/lib/irb/init.rb
+++ b/lib/irb/init.rb
@@ -1,16 +1,50 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/init.rb - irb initialize module
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB # :nodoc:
+ @CONF = {}
+ @INITIALIZED = false
+ # Displays current configuration.
+ #
+ # Modifying the configuration is achieved by sending a message to IRB.conf.
+ #
+ # See IRB@Configuration for more information.
+ def IRB.conf
+ @CONF
+ end
+
+ def @CONF.inspect
+ array = []
+ for k, v in sort{|a1, a2| a1[0].id2name <=> a2[0].id2name}
+ case k
+ when :MAIN_CONTEXT, :__TMP__EHV__
+ array.push format("CONF[:%s]=...myself...", k.id2name)
+ when :PROMPT
+ s = v.collect{
+ |kk, vv|
+ ss = vv.collect{|kkk, vvv| ":#{kkk.id2name}=>#{vvv.inspect}"}
+ format(":%s=>{%s}", kk.id2name, ss.join(", "))
+ }
+ array.push format("CONF[:%s]={%s}", k.id2name, s.join(", "))
+ else
+ array.push format("CONF[:%s]=%s", k.id2name, v.inspect)
+ end
+ end
+ array.join("\n")
+ end
+
+ # Returns the current version of IRB, including release version and last
+ # updated date.
+ def IRB.version
+ format("irb %s (%s)", @RELEASE_VERSION, @LAST_UPDATE_DATE)
+ end
+
+ def IRB.initialized?
+ !!@INITIALIZED
+ end
# initialize config
def IRB.setup(ap_path, argv: ::ARGV)
@@ -23,17 +57,16 @@ module IRB # :nodoc:
unless @CONF[:PROMPT][@CONF[:PROMPT_MODE]]
fail UndefinedPromptMode, @CONF[:PROMPT_MODE]
end
+ @INITIALIZED = true
end
# @CONF default setting
def IRB.init_config(ap_path)
- # class instance variables
- @TRACER_INITIALIZED = false
-
# default configurations
unless ap_path and @CONF[:AP_NAME]
ap_path = File.join(File.dirname(File.dirname(__FILE__)), "irb.rb")
end
+ @CONF[:VERSION] = version
@CONF[:AP_NAME] = File::basename(ap_path, ".rb")
@CONF[:IRB_NAME] = "irb"
@@ -44,13 +77,15 @@ module IRB # :nodoc:
@CONF[:IRB_RC] = nil
@CONF[:USE_SINGLELINE] = false unless defined?(ReadlineInputMethod)
- @CONF[:USE_COLORIZE] = !ENV['NO_COLOR']
- @CONF[:USE_AUTOCOMPLETE] = true
+ @CONF[:USE_COLORIZE] = (nc = ENV['NO_COLOR']).nil? || nc.empty?
+ @CONF[:USE_AUTOCOMPLETE] = ENV.fetch("IRB_USE_AUTOCOMPLETE", "true") != "false"
+ @CONF[:COMPLETOR] = ENV.fetch("IRB_COMPLETOR", "regexp").to_sym
@CONF[:INSPECT_MODE] = true
@CONF[:USE_TRACER] = false
@CONF[:USE_LOADER] = false
@CONF[:IGNORE_SIGINT] = true
@CONF[:IGNORE_EOF] = false
+ @CONF[:USE_PAGER] = true
@CONF[:EXTRA_DOC_DIRS] = []
@CONF[:ECHO] = nil
@CONF[:ECHO_ON_ASSIGNMENT] = nil
@@ -64,35 +99,30 @@ module IRB # :nodoc:
@CONF[:PROMPT] = {
:NULL => {
:PROMPT_I => nil,
- :PROMPT_N => nil,
:PROMPT_S => nil,
:PROMPT_C => nil,
:RETURN => "%s\n"
},
:DEFAULT => {
- :PROMPT_I => "%N(%m):%03n:%i> ",
- :PROMPT_N => "%N(%m):%03n:%i> ",
- :PROMPT_S => "%N(%m):%03n:%i%l ",
- :PROMPT_C => "%N(%m):%03n:%i* ",
+ :PROMPT_I => "%N(%m):%03n> ",
+ :PROMPT_S => "%N(%m):%03n%l ",
+ :PROMPT_C => "%N(%m):%03n* ",
:RETURN => "=> %s\n"
},
:CLASSIC => {
:PROMPT_I => "%N(%m):%03n:%i> ",
- :PROMPT_N => "%N(%m):%03n:%i> ",
:PROMPT_S => "%N(%m):%03n:%i%l ",
:PROMPT_C => "%N(%m):%03n:%i* ",
:RETURN => "%s\n"
},
:SIMPLE => {
:PROMPT_I => ">> ",
- :PROMPT_N => ">> ",
:PROMPT_S => "%l> ",
:PROMPT_C => "?> ",
:RETURN => "=> %s\n"
},
:INF_RUBY => {
- :PROMPT_I => "%N(%m):%03n:%i> ",
- :PROMPT_N => nil,
+ :PROMPT_I => "%N(%m):%03n> ",
:PROMPT_S => nil,
:PROMPT_C => nil,
:RETURN => "%s\n",
@@ -100,7 +130,6 @@ module IRB # :nodoc:
},
:XMP => {
:PROMPT_I => nil,
- :PROMPT_N => nil,
:PROMPT_S => nil,
:PROMPT_C => nil,
:RETURN => " ==>%s\n"
@@ -158,6 +187,12 @@ module IRB # :nodoc:
@CONF[:LC_MESSAGES] = Locale.new
@CONF[:AT_EXIT] = []
+
+ @CONF[:COMMAND_ALIASES] = {
+ # Symbol aliases
+ :'$' => :show_source,
+ :'@' => :whereami,
+ }
end
def IRB.set_measure_callback(type = nil, arg = nil, &block)
@@ -183,6 +218,7 @@ module IRB # :nodoc:
added = [:TIME, IRB.conf[:MEASURE_PROC][:TIME], arg]
end
if added
+ IRB.conf[:MEASURE] = true
found = IRB.conf[:MEASURE_CALLBACKS].find{ |m| m[0] == added[0] && m[2] == added[2] }
if found
# already added
@@ -203,6 +239,7 @@ module IRB # :nodoc:
type_sym = type.upcase.to_sym
IRB.conf[:MEASURE_CALLBACKS].reject!{ |t, | t == type_sym }
end
+ IRB.conf[:MEASURE] = nil if IRB.conf[:MEASURE_CALLBACKS].empty?
end
def IRB.init_error
@@ -250,13 +287,27 @@ module IRB # :nodoc:
end
when "--noinspect"
@CONF[:INSPECT_MODE] = false
+ when "--no-pager"
+ @CONF[:USE_PAGER] = false
when "--singleline", "--readline", "--legacy"
@CONF[:USE_SINGLELINE] = true
when "--nosingleline", "--noreadline"
@CONF[:USE_SINGLELINE] = false
when "--multiline", "--reidline"
+ if opt == "--reidline"
+ warn <<~MSG.strip
+ --reidline is deprecated, please use --multiline instead.
+ MSG
+ end
+
@CONF[:USE_MULTILINE] = true
when "--nomultiline", "--noreidline"
+ if opt == "--noreidline"
+ warn <<~MSG.strip
+ --noreidline is deprecated, please use --nomultiline instead.
+ MSG
+ end
+
@CONF[:USE_MULTILINE] = false
when /^--extra-doc-dir(?:=(.+))?/
opt = $1 || argv.shift
@@ -283,12 +334,20 @@ module IRB # :nodoc:
@CONF[:USE_AUTOCOMPLETE] = true
when "--noautocomplete"
@CONF[:USE_AUTOCOMPLETE] = false
+ when "--regexp-completor"
+ @CONF[:COMPLETOR] = :regexp
+ when "--type-completor"
+ @CONF[:COMPLETOR] = :type
when /^--prompt-mode(?:=(.+))?/, /^--prompt(?:=(.+))?/
opt = $1 || argv.shift
prompt_mode = opt.upcase.tr("-", "_").intern
@CONF[:PROMPT_MODE] = prompt_mode
when "--noprompt"
@CONF[:PROMPT_MODE] = :NULL
+ when "--script"
+ noscript = false
+ when "--noscript"
+ noscript = true
when "--inf-ruby-mode"
@CONF[:PROMPT_MODE] = :INF_RUBY
when "--sample-book-mode", "--simple-prompt"
@@ -309,16 +368,20 @@ module IRB # :nodoc:
IRB.print_usage
exit 0
when "--"
- if opt = argv.shift
+ if !noscript && (opt = argv.shift)
@CONF[:SCRIPT] = opt
$0 = opt
end
break
- when /^-/
+ when /^-./
fail UnrecognizedSwitch, opt
else
- @CONF[:SCRIPT] = opt
- $0 = opt
+ if noscript
+ argv.unshift(opt)
+ else
+ @CONF[:SCRIPT] = opt
+ $0 = opt
+ end
break
end
end
@@ -329,63 +392,39 @@ module IRB # :nodoc:
$LOAD_PATH.unshift(*load_path)
end
- # running config
+ # Run the config file
def IRB.run_config
if @CONF[:RC]
- begin
- load rc_file
- rescue LoadError, Errno::ENOENT
- rescue # StandardError, ScriptError
- print "load error: #{rc_file}\n"
- print $!.class, ": ", $!, "\n"
- for err in $@[0, $@.size - 2]
- print "\t", err, "\n"
- end
+ irbrc_files.each do |rc|
+ load rc
+ rescue StandardError, ScriptError => e
+ warn "Error loading RC file '#{rc}':\n#{e.full_message(highlight: false)}"
end
end
end
IRBRC_EXT = "rc"
- def IRB.rc_file(ext = IRBRC_EXT)
- if !@CONF[:RC_NAME_GENERATOR]
- rc_file_generators do |rcgen|
- @CONF[:RC_NAME_GENERATOR] ||= rcgen
- if File.exist?(rcgen.call(IRBRC_EXT))
- @CONF[:RC_NAME_GENERATOR] = rcgen
- break
- end
- end
+
+ def IRB.rc_file(ext)
+ prepare_irbrc_name_generators
+
+ # When irbrc exist in default location
+ if (rcgen = @existing_rc_name_generators.first)
+ return rcgen.call(ext)
end
- case rc_file = @CONF[:RC_NAME_GENERATOR].call(ext)
- when String
- return rc_file
- else
- fail IllegalRCNameGenerator
+
+ # When irbrc does not exist in default location
+ rc_file_generators do |rcgen|
+ return rcgen.call(ext)
end
+
+ # When HOME and XDG_CONFIG_HOME are not available
+ nil
end
- # enumerate possible rc-file base name generators
- def IRB.rc_file_generators
- if irbrc = ENV["IRBRC"]
- yield proc{|rc| rc == "rc" ? irbrc : irbrc+rc}
- end
- if xdg_config_home = ENV["XDG_CONFIG_HOME"]
- irb_home = File.join(xdg_config_home, "irb")
- unless File.exist? irb_home
- require 'fileutils'
- FileUtils.mkdir_p irb_home
- end
- yield proc{|rc| irb_home + "/irb#{rc}"}
- end
- if home = ENV["HOME"]
- yield proc{|rc| home+"/.irb#{rc}"}
- end
- current_dir = Dir.pwd
- yield proc{|rc| current_dir+"/.config/irb/irb#{rc}"}
- yield proc{|rc| current_dir+"/.irb#{rc}"}
- yield proc{|rc| current_dir+"/irb#{rc.sub(/\A_?/, '.')}"}
- yield proc{|rc| current_dir+"/_irb#{rc}"}
- yield proc{|rc| current_dir+"/$irb#{rc}"}
+ def IRB.irbrc_files
+ prepare_irbrc_name_generators
+ @irbrc_files
end
# loading modules
@@ -399,10 +438,52 @@ module IRB # :nodoc:
end
end
-
- DefaultEncodings = Struct.new(:external, :internal)
class << IRB
private
+
+ def prepare_irbrc_name_generators
+ return if @existing_rc_name_generators
+
+ @existing_rc_name_generators = []
+ @irbrc_files = []
+ rc_file_generators do |rcgen|
+ irbrc = rcgen.call(IRBRC_EXT)
+ if File.exist?(irbrc)
+ @irbrc_files << irbrc
+ @existing_rc_name_generators << rcgen
+ end
+ end
+ generate_current_dir_irbrc_files.each do |irbrc|
+ @irbrc_files << irbrc if File.exist?(irbrc)
+ end
+ @irbrc_files.uniq!
+ end
+
+ # enumerate possible rc-file base name generators
+ def rc_file_generators
+ if irbrc = ENV["IRBRC"]
+ yield proc{|rc| rc == "rc" ? irbrc : irbrc+rc}
+ end
+ if xdg_config_home = ENV["XDG_CONFIG_HOME"]
+ irb_home = File.join(xdg_config_home, "irb")
+ if File.directory?(irb_home)
+ yield proc{|rc| irb_home + "/irb#{rc}"}
+ end
+ end
+ if home = ENV["HOME"]
+ yield proc{|rc| home+"/.irb#{rc}"}
+ if xdg_config_home.nil? || xdg_config_home.empty?
+ yield proc{|rc| home+"/.config/irb/irb#{rc}"}
+ end
+ end
+ end
+
+ # possible irbrc files in current directory
+ def generate_current_dir_irbrc_files
+ current_dir = Dir.pwd
+ %w[.irbrc irbrc _irbrc $irbrc].map { |file| "#{current_dir}/#{file}" }
+ end
+
def set_encoding(extern, intern = nil, override: true)
verbose, $VERBOSE = $VERBOSE, nil
Encoding.default_external = extern unless extern.nil? || extern.empty?
diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb
index fd68239ee3..e5adb350e8 100644
--- a/lib/irb/input-method.rb
+++ b/lib/irb/input-method.rb
@@ -1,31 +1,17 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/input-method.rb - input methods used irb
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
-require_relative 'src_encoding'
-require_relative 'magic-file'
+
require_relative 'completion'
+require_relative "history"
require 'io/console'
require 'reline'
-require 'rdoc'
module IRB
- STDIN_FILE_NAME = "(line)" # :nodoc:
class InputMethod
-
- # Creates a new input method object
- def initialize(file = STDIN_FILE_NAME)
- @file_name = file
- end
- # The file name of this input method, usually given during initialization.
- attr_reader :file_name
+ BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{("
# The irb prompt associated with this input method
attr_accessor :prompt
@@ -34,7 +20,7 @@ module IRB
#
# See IO#gets for more information.
def gets
- fail NotImplementedError, "gets"
+ fail NotImplementedError
end
public :gets
@@ -54,6 +40,14 @@ module IRB
false
end
+ def support_history_saving?
+ false
+ end
+
+ def prompting?
+ false
+ end
+
# For debug message
def inspect
'Abstract InputMethod'
@@ -63,7 +57,6 @@ module IRB
class StdioInputMethod < InputMethod
# Creates a new input method object
def initialize
- super
@line_no = 0
@line = []
@stdin = IO.open(STDIN.to_i, :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
@@ -102,6 +95,10 @@ module IRB
true
end
+ def prompting?
+ STDIN.tty?
+ end
+
# Returns the current line number for #io.
#
# #line counts the number of times #gets is called.
@@ -137,12 +134,9 @@ module IRB
# Creates a new input method object
def initialize(file)
- super
- @io = IRB::MagicFile.open(file)
+ @io = file.is_a?(IO) ? file : File.open(file)
@external_encoding = @io.external_encoding
end
- # The file name of this input method, usually given during initialization.
- attr_reader :file_name
# Whether the end of this input method has been reached, returns +true+ if
# there is no more data to read.
@@ -175,132 +169,128 @@ module IRB
end
end
- begin
- class ReadlineInputMethod < InputMethod
- def self.initialize_readline
- require "readline"
- rescue LoadError
- else
- include ::Readline
- end
+ class ReadlineInputMethod < StdioInputMethod
+ def self.initialize_readline
+ require "readline"
+ rescue LoadError
+ else
+ include ::Readline
+ end
- # Creates a new input method object using Readline
- def initialize
- self.class.initialize_readline
- if Readline.respond_to?(:encoding_system_needs)
- IRB.__send__(:set_encoding, Readline.encoding_system_needs.name, override: false)
- end
- super
+ include HistorySavingAbility
- @line_no = 0
- @line = []
- @eof = false
+ # Creates a new input method object using Readline
+ def initialize
+ self.class.initialize_readline
+ if Readline.respond_to?(:encoding_system_needs)
+ IRB.__send__(:set_encoding, Readline.encoding_system_needs.name, override: false)
+ end
- @stdin = IO.open(STDIN.to_i, :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
- @stdout = IO.open(STDOUT.to_i, 'w', :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
+ super
- if Readline.respond_to?("basic_word_break_characters=")
- Readline.basic_word_break_characters = IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS
- end
- Readline.completion_append_character = nil
- Readline.completion_proc = IRB::InputCompletor::CompletionProc
- end
+ @eof = false
+ @completor = RegexpCompletor.new
- # Reads the next line from this input method.
- #
- # See IO#gets for more information.
- def gets
- Readline.input = @stdin
- Readline.output = @stdout
- if l = readline(@prompt, false)
- HISTORY.push(l) if !l.empty?
- @line[@line_no += 1] = l + "\n"
- else
- @eof = true
- l
- end
+ if Readline.respond_to?("basic_word_break_characters=")
+ Readline.basic_word_break_characters = BASIC_WORD_BREAK_CHARACTERS
end
+ Readline.completion_append_character = nil
+ Readline.completion_proc = ->(target) {
+ bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
+ @completor.completion_candidates('', target, '', bind: bind)
+ }
+ end
- # Whether the end of this input method has been reached, returns +true+
- # if there is no more data to read.
- #
- # See IO#eof? for more information.
- def eof?
- @eof
- end
+ def completion_info
+ 'RegexpCompletor'
+ end
- # Whether this input method is still readable when there is no more data to
- # read.
- #
- # See IO#eof for more information.
- def readable_after_eof?
- true
+ # Reads the next line from this input method.
+ #
+ # See IO#gets for more information.
+ def gets
+ Readline.input = @stdin
+ Readline.output = @stdout
+ if l = readline(@prompt, false)
+ HISTORY.push(l) if !l.empty?
+ @line[@line_no += 1] = l + "\n"
+ else
+ @eof = true
+ l
end
+ end
- # Returns the current line number for #io.
- #
- # #line counts the number of times #gets is called.
- #
- # See IO#lineno for more information.
- def line(line_no)
- @line[line_no]
- end
+ # Whether the end of this input method has been reached, returns +true+
+ # if there is no more data to read.
+ #
+ # See IO#eof? for more information.
+ def eof?
+ @eof
+ end
- # The external encoding for standard input.
- def encoding
- @stdin.external_encoding
- end
+ def prompting?
+ true
+ end
- # For debug message
- def inspect
- readline_impl = (defined?(Reline) && Readline == Reline) ? 'Reline' : 'ext/readline'
- str = "ReadlineInputMethod with #{readline_impl} #{Readline::VERSION}"
- inputrc_path = File.expand_path(ENV['INPUTRC'] || '~/.inputrc')
- str += " and #{inputrc_path}" if File.exist?(inputrc_path)
- str
- end
+ # For debug message
+ def inspect
+ readline_impl = (defined?(Reline) && Readline == Reline) ? 'Reline' : 'ext/readline'
+ str = "ReadlineInputMethod with #{readline_impl} #{Readline::VERSION}"
+ inputrc_path = File.expand_path(ENV['INPUTRC'] || '~/.inputrc')
+ str += " and #{inputrc_path}" if File.exist?(inputrc_path)
+ str
end
end
- class ReidlineInputMethod < InputMethod
- include Reline
-
+ class RelineInputMethod < StdioInputMethod
+ HISTORY = Reline::HISTORY
+ include HistorySavingAbility
# Creates a new input method object using Reline
- def initialize
+ def initialize(completor)
IRB.__send__(:set_encoding, Reline.encoding_system_needs.name, override: false)
- super
- @line_no = 0
- @line = []
- @eof = false
+ super()
- @stdin = ::IO.open(STDIN.to_i, :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
- @stdout = ::IO.open(STDOUT.to_i, 'w', :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-")
+ @eof = false
+ @completor = completor
- if Reline.respond_to?("basic_word_break_characters=")
- Reline.basic_word_break_characters = IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS
- end
+ Reline.basic_word_break_characters = BASIC_WORD_BREAK_CHARACTERS
Reline.completion_append_character = nil
Reline.completer_quote_characters = ''
- Reline.completion_proc = IRB::InputCompletor::CompletionProc
+ Reline.completion_proc = ->(target, preposing, postposing) {
+ bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
+ @completion_params = [preposing, target, postposing, bind]
+ @completor.completion_candidates(preposing, target, postposing, bind: bind)
+ }
Reline.output_modifier_proc =
if IRB.conf[:USE_COLORIZE]
proc do |output, complete: |
next unless IRB::Color.colorable?
- IRB::Color.colorize_code(output, complete: complete)
+ lvars = IRB.CurrentContext&.local_variables || []
+ IRB::Color.colorize_code(output, complete: complete, local_variables: lvars)
end
else
proc do |output|
Reline::Unicode.escape_for_print(output)
end
end
- Reline.dig_perfect_match_proc = IRB::InputCompletor::PerfectMatchedProc
+ Reline.dig_perfect_match_proc = ->(matched) { display_document(matched) }
Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE]
+
if IRB.conf[:USE_AUTOCOMPLETE]
- Reline.add_dialog_proc(:show_doc, SHOW_DOC_DIALOG, Reline::DEFAULT_DIALOG_CONTEXT)
+ begin
+ require 'rdoc'
+ Reline.add_dialog_proc(:show_doc, show_doc_dialog_proc, Reline::DEFAULT_DIALOG_CONTEXT)
+ rescue LoadError
+ end
end
end
+ def completion_info
+ autocomplete_message = Reline.autocompletion ? 'Autocomplete' : 'Tab Complete'
+ "#{autocomplete_message}, #{@completor.inspect}"
+ end
+
def check_termination(&block)
@check_termination_proc = block
end
@@ -313,98 +303,164 @@ module IRB
@auto_indent_proc = block
end
- SHOW_DOC_DIALOG = ->() {
- dialog.trap_key = nil
- alt_d = [
- [Reline::Key.new(nil, 0xE4, true)], # Normal Alt+d.
- [27, 100], # Normal Alt+d when convert-meta isn't used.
- [195, 164], # The "ä" that appears when Alt+d is pressed on xterm.
- [226, 136, 130] # The "∂" that appears when Alt+d in pressed on iTerm2.
- ]
+ def retrieve_doc_namespace(matched)
+ preposing, _target, postposing, bind = @completion_params
+ @completor.doc_namespace(preposing, matched, postposing, bind: bind)
+ end
- if just_cursor_moving and completion_journey_data.nil?
- return nil
- end
- cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4)
- return nil if result.nil? or pointer.nil? or pointer < 0
- name = result[pointer]
- name = IRB::InputCompletor.retrieve_completion_data(name, doc_namespace: true)
+ def rdoc_ri_driver
+ return @rdoc_ri_driver if defined?(@rdoc_ri_driver)
- options = {}
- options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty?
- driver = RDoc::RI::Driver.new(options)
+ begin
+ require 'rdoc'
+ rescue LoadError
+ @rdoc_ri_driver = nil
+ else
+ options = {}
+ options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty?
+ @rdoc_ri_driver = RDoc::RI::Driver.new(options)
+ end
+ end
- if key.match?(dialog.name)
- begin
- driver.display_names([name])
- rescue RDoc::RI::Driver::NotFoundError
+ def show_doc_dialog_proc
+ input_method = self # self is changed in the lambda below.
+ ->() {
+ dialog.trap_key = nil
+ alt_d = [
+ [Reline::Key.new(nil, 0xE4, true)], # Normal Alt+d.
+ [27, 100], # Normal Alt+d when convert-meta isn't used.
+ [195, 164], # The "ä" that appears when Alt+d is pressed on xterm.
+ [226, 136, 130] # The "∂" that appears when Alt+d in pressed on iTerm2.
+ ]
+
+ if just_cursor_moving and completion_journey_data.nil?
+ return nil
end
- end
+ cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4)
+ return nil if result.nil? or pointer.nil? or pointer < 0
- begin
- name = driver.expand_name(name)
- rescue RDoc::RI::Driver::NotFoundError
- return nil
- rescue
- return nil # unknown error
- end
- doc = nil
- used_for_class = false
- if not name =~ /#|\./
- found, klasses, includes, extends = driver.classes_and_includes_and_extends_for(name)
- if not found.empty?
- doc = driver.class_document(name, found, klasses, includes, extends)
- used_for_class = true
+ name = input_method.retrieve_doc_namespace(result[pointer])
+ # Use first one because document dialog does not support multiple namespaces.
+ name = name.first if name.is_a?(Array)
+
+ show_easter_egg = name&.match?(/\ARubyVM/) && !ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER']
+
+ driver = input_method.rdoc_ri_driver
+
+ if key.match?(dialog.name)
+ if show_easter_egg
+ IRB.__send__(:easter_egg)
+ else
+ begin
+ driver.display_names([name])
+ rescue RDoc::RI::Driver::NotFoundError
+ end
+ end
end
- end
- unless used_for_class
- doc = RDoc::Markup::Document.new
+
begin
- driver.add_method(doc, name)
+ name = driver.expand_name(name)
rescue RDoc::RI::Driver::NotFoundError
- doc = nil
+ return nil
rescue
return nil # unknown error
end
- end
- return nil if doc.nil?
- width = 40
-
- right_x = cursor_pos_to_render.x + autocomplete_dialog.width
- if right_x + width > screen_width
- right_width = screen_width - (right_x + 1)
- left_x = autocomplete_dialog.column - width
- left_x = 0 if left_x < 0
- left_width = width > autocomplete_dialog.column ? autocomplete_dialog.column : width
- if right_width.positive? and left_width.positive?
- if right_width >= left_width
+ doc = nil
+ used_for_class = false
+ if not name =~ /#|\./
+ found, klasses, includes, extends = driver.classes_and_includes_and_extends_for(name)
+ if not found.empty?
+ doc = driver.class_document(name, found, klasses, includes, extends)
+ used_for_class = true
+ end
+ end
+ unless used_for_class
+ doc = RDoc::Markup::Document.new
+ begin
+ driver.add_method(doc, name)
+ rescue RDoc::RI::Driver::NotFoundError
+ doc = nil
+ rescue
+ return nil # unknown error
+ end
+ end
+ return nil if doc.nil?
+ width = 40
+
+ right_x = cursor_pos_to_render.x + autocomplete_dialog.width
+ if right_x + width > screen_width
+ right_width = screen_width - (right_x + 1)
+ left_x = autocomplete_dialog.column - width
+ left_x = 0 if left_x < 0
+ left_width = width > autocomplete_dialog.column ? autocomplete_dialog.column : width
+ if right_width.positive? and left_width.positive?
+ if right_width >= left_width
+ width = right_width
+ x = right_x
+ else
+ width = left_width
+ x = left_x
+ end
+ elsif right_width.positive? and left_width <= 0
width = right_width
x = right_x
- else
+ elsif right_width <= 0 and left_width.positive?
width = left_width
x = left_x
+ else # Both are negative width.
+ return nil
end
- elsif right_width.positive? and left_width <= 0
- width = right_width
+ else
x = right_x
- elsif right_width <= 0 and left_width.positive?
- width = left_width
- x = left_x
- else # Both are negative width.
- return nil
end
- else
- x = right_x
+ formatter = RDoc::Markup::ToAnsi.new
+ formatter.width = width
+ dialog.trap_key = alt_d
+ mod_key = RUBY_PLATFORM.match?(/darwin/) ? "Option" : "Alt"
+ if show_easter_egg
+ type = STDOUT.external_encoding == Encoding::UTF_8 ? :unicode : :ascii
+ contents = IRB.send(:easter_egg_logo, type).split("\n")
+ message = "Press #{mod_key}+d to see more"
+ contents[0][0, message.size] = message
+ else
+ message = "Press #{mod_key}+d to read the full document"
+ contents = [message] + doc.accept(formatter).split("\n")
+ end
+ contents = contents.take(preferred_dialog_height)
+
+ y = cursor_pos_to_render.y
+ Reline::DialogRenderInfo.new(pos: Reline::CursorPos.new(x, y), contents: contents, width: width, bg_color: '49')
+ }
+ end
+
+ def display_document(matched)
+ driver = rdoc_ri_driver
+ return unless driver
+
+ if matched =~ /\A(?:::)?RubyVM/ and not ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER']
+ IRB.__send__(:easter_egg)
+ return
end
- formatter = RDoc::Markup::ToAnsi.new
- formatter.width = width
- dialog.trap_key = alt_d
- message = 'Press Alt+d to read the full document'
- contents = [message] + doc.accept(formatter).split("\n")
- y = cursor_pos_to_render.y
- DialogRenderInfo.new(pos: Reline::CursorPos.new(x, y), contents: contents, width: width, bg_color: '49')
- }
+ namespace = retrieve_doc_namespace(matched)
+ return unless namespace
+
+ if namespace.is_a?(Array)
+ out = RDoc::Markup::Document.new
+ namespace.each do |m|
+ begin
+ driver.add_method(out, m)
+ rescue RDoc::RI::Driver::NotFoundError
+ end
+ end
+ driver.display(out)
+ else
+ begin
+ driver.display_names([namespace])
+ rescue RDoc::RI::Driver::NotFoundError
+ end
+ end
+ end
# Reads the next line from this input method.
#
@@ -414,8 +470,8 @@ module IRB
Reline.output = @stdout
Reline.prompt_proc = @prompt_proc
Reline.auto_indent_proc = @auto_indent_proc if @auto_indent_proc
- if l = readmultiline(@prompt, false, &@check_termination_proc)
- HISTORY.push(l) if !l.empty?
+ if l = Reline.readmultiline(@prompt, false, &@check_termination_proc)
+ Reline::HISTORY.push(l) if !l.empty?
@line[@line_no += 1] = l + "\n"
else
@eof = true
@@ -431,39 +487,26 @@ module IRB
@eof
end
- # Whether this input method is still readable when there is no more data to
- # read.
- #
- # See IO#eof for more information.
- def readable_after_eof?
+ def prompting?
true
end
- # Returns the current line number for #io.
- #
- # #line counts the number of times #gets is called.
- #
- # See IO#lineno for more information.
- def line(line_no)
- @line[line_no]
- end
-
- # The external encoding for standard input.
- def encoding
- @stdin.external_encoding
- end
-
# For debug message
def inspect
config = Reline::Config.new
- str = "ReidlineInputMethod with Reline #{Reline::VERSION}"
- if config.respond_to?(:inputrc_path)
- inputrc_path = File.expand_path(config.inputrc_path)
- else
- inputrc_path = File.expand_path(ENV['INPUTRC'] || '~/.inputrc')
- end
+ str = "RelineInputMethod with Reline #{Reline::VERSION}"
+ inputrc_path = File.expand_path(config.inputrc_path)
str += " and #{inputrc_path}" if File.exist?(inputrc_path)
str
end
end
+
+ class ReidlineInputMethod < RelineInputMethod
+ def initialize
+ warn <<~MSG.strip
+ IRB::ReidlineInputMethod is deprecated, please use IRB::RelineInputMethod instead.
+ MSG
+ super
+ end
+ end
end
diff --git a/lib/irb/inspector.rb b/lib/irb/inspector.rb
index d8c0ba90cf..667087ccba 100644
--- a/lib/irb/inspector.rb
+++ b/lib/irb/inspector.rb
@@ -1,15 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/inspector.rb - inspect methods
-# $Release Version: 0.9.6$
-# $Revision: 1.19 $
-# $Date: 2002/06/11 07:51:31 $
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB # :nodoc:
@@ -42,6 +35,7 @@ module IRB # :nodoc:
# irb(main):001:0> "what?" #=> omg! what?
#
class Inspector
+ KERNEL_INSPECT = Object.instance_method(:inspect)
# Default inspectors available to irb, this includes:
#
# +:pp+:: Using Kernel#pretty_inspect
@@ -52,7 +46,7 @@ module IRB # :nodoc:
# Determines the inspector to use where +inspector+ is one of the keys passed
# during inspector definition.
def self.keys_with_inspector(inspector)
- INSPECTORS.select{|k,v| v == inspector}.collect{|k, v| k}
+ INSPECTORS.select{|k, v| v == inspector}.collect{|k, v| k}
end
# Example
@@ -100,9 +94,17 @@ module IRB # :nodoc:
# Proc to call when the input is evaluated and output in irb.
def inspect_value(v)
@inspect.call(v)
- rescue
- puts "(Object doesn't support #inspect)"
- ''
+ rescue => e
+ puts "An error occurred when inspecting the object: #{e.inspect}"
+
+ begin
+ puts "Result of Kernel#inspect: #{KERNEL_INSPECT.bind_call(v)}"
+ ''
+ rescue => e
+ puts "An error occurred when running Kernel#inspect: #{e.inspect}"
+ puts e.backtrace.join("\n")
+ ''
+ end
end
end
@@ -111,7 +113,7 @@ module IRB # :nodoc:
Color.colorize_code(v.inspect, colorable: Color.colorable? && Color.inspect_colorable?(v))
}
Inspector.def_inspector([true, :pp, :pretty_inspect], proc{require_relative "color_printer"}){|v|
- IRB::ColorPrinter.pp(v, '').chomp
+ IRB::ColorPrinter.pp(v, +'').chomp
}
Inspector.def_inspector([:yaml, :YAML], proc{require "yaml"}){|v|
begin
diff --git a/lib/irb/irb.gemspec b/lib/irb/irb.gemspec
index 26d0fb018f..b29002f593 100644
--- a/lib/irb/irb.gemspec
+++ b/lib/irb/irb.gemspec
@@ -8,14 +8,19 @@ end
Gem::Specification.new do |spec|
spec.name = "irb"
spec.version = IRB::VERSION
- spec.authors = ["Keiju ISHITSUKA"]
- spec.email = ["keiju@ruby-lang.org"]
+ spec.authors = ["aycabta", "Keiju ISHITSUKA"]
+ spec.email = ["aycabta@gmail.com", "keiju@ruby-lang.org"]
spec.summary = %q{Interactive Ruby command-line tool for REPL (Read Eval Print Loop).}
spec.description = %q{Interactive Ruby command-line tool for REPL (Read Eval Print Loop).}
spec.homepage = "https://github.com/ruby/irb"
spec.licenses = ["Ruby", "BSD-2-Clause"]
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+ spec.metadata["documentation_uri"] = spec.homepage
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
+
spec.files = [
".document",
"Gemfile",
@@ -34,7 +39,8 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5")
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7")
- spec.add_dependency "reline", ">= 0.3.0"
+ spec.add_dependency "reline", ">= 0.4.2"
+ spec.add_dependency "rdoc", ">= 4.0.0"
end
diff --git a/lib/irb/lc/error.rb b/lib/irb/lc/error.rb
index b8a7fe5a0e..ee0f047822 100644
--- a/lib/irb/lc/error.rb
+++ b/lib/irb/lc/error.rb
@@ -1,14 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/lc/error.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB
# :stopdoc:
@@ -18,11 +12,6 @@ module IRB
super("Unrecognized switch: #{val}")
end
end
- class NotImplementedError < StandardError
- def initialize(val)
- super("Need to define `#{val}'")
- end
- end
class CantReturnToNormalMode < StandardError
def initialize
super("Can't return to normal mode.")
@@ -48,11 +37,6 @@ module IRB
super("No such job(#{val}).")
end
end
- class CantShiftToMultiIrbMode < StandardError
- def initialize
- super("Can't shift to multi irb mode.")
- end
- end
class CantChangeBinding < StandardError
def initialize(val)
super("Can't change binding to (#{val}).")
@@ -63,11 +47,6 @@ module IRB
super("Undefined prompt mode(#{val}).")
end
end
- class IllegalRCGenerator < StandardError
- def initialize
- super("Define illegal RC_NAME_GENERATOR.")
- end
- end
# :startdoc:
end
diff --git a/lib/irb/lc/help-message b/lib/irb/lc/help-message
index 939dc38975..37347306e8 100644
--- a/lib/irb/lc/help-message
+++ b/lib/irb/lc/help-message
@@ -1,61 +1,55 @@
-# -*- coding: utf-8 -*-
-#
-# irb/lc/help-message.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
Usage: irb.rb [options] [programfile] [arguments]
- -f Suppress read of ~/.irbrc
- -d Set $DEBUG to true (same as `ruby -d')
- -r load-module Same as `ruby -r'
- -I path Specify $LOAD_PATH directory
- -U Same as `ruby -U`
- -E enc Same as `ruby -E`
- -w Same as `ruby -w`
- -W[level=2] Same as `ruby -W`
+ -f Don't initialize from configuration file.
+ -d Set $DEBUG and $VERBOSE to true (same as 'ruby -d').
+ -r load-module Require load-module (same as 'ruby -r').
+ -I path Specify $LOAD_PATH directory (same as 'ruby -I').
+ -U Set external and internal encodings to UTF-8.
+ -E ex[:in] Set default external (ex) and internal (in) encodings
+ (same as 'ruby -E').
+ -w Suppress warnings (same as 'ruby -w').
+ -W[level=2] Set warning level: 0=silence, 1=medium, 2=verbose
+ (same as 'ruby -W').
--context-mode n Set n[0-4] to method to create Binding Object,
- when new workspace was created
- --extra-doc-dir Add an extra doc dir for the doc dialog
- --echo Show result (default)
- --noecho Don't show result
+ when new workspace was created.
+ --extra-doc-dir Add an extra doc dir for the doc dialog.
+ --echo Show result (default).
+ --noecho Don't show result.
--echo-on-assignment
- Show result on assignment
+ Show result on assignment.
--noecho-on-assignment
- Don't show result on assignment
+ Don't show result on assignment.
--truncate-echo-on-assignment
- Show truncated result on assignment (default)
- --inspect Use `inspect' for output
- --noinspect Don't use inspect for output
- --multiline Use multiline editor module
- --nomultiline Don't use multiline editor module
- --singleline Use singleline editor module
- --nosingleline Don't use singleline editor module
- --colorize Use colorization
- --nocolorize Don't use colorization
- --autocomplete Use autocompletion
- --noautocomplete Don't use autocompletion
- --prompt prompt-mode/--prompt-mode prompt-mode
- Switch prompt mode. Pre-defined prompt modes are
- `default', `simple', `xmp' and `inf-ruby'
+ Show truncated result on assignment (default).
+ --inspect Use 'inspect' for output.
+ --noinspect Don't use 'inspect' for output.
+ --no-pager Don't use pager.
+ --multiline Use multiline editor module (default).
+ --nomultiline Don't use multiline editor module.
+ --singleline Use single line editor module.
+ --nosingleline Don't use single line editor module (default).
+ --colorize Use color-highlighting (default).
+ --nocolorize Don't use color-highlighting.
+ --autocomplete Use auto-completion (default).
+ --noautocomplete Don't use auto-completion.
+ --regexp-completor
+ Use regexp based completion (default).
+ --type-completor Use type based completion.
+ --prompt prompt-mode, --prompt-mode prompt-mode
+ Set prompt mode. Pre-defined prompt modes are:
+ 'default', 'classic', 'simple', 'inf-ruby', 'xmp', 'null'.
--inf-ruby-mode Use prompt appropriate for inf-ruby-mode on emacs.
Suppresses --multiline and --singleline.
- --sample-book-mode/--simple-prompt
- Simple prompt mode
- --noprompt No prompt mode
+ --sample-book-mode, --simple-prompt
+ Set prompt mode to 'simple'.
+ --noprompt Don't output prompt.
+ --script Script mode (default, treat first argument as script)
+ --noscript No script mode (leave arguments in argv)
--single-irb Share self with sub-irb.
- --tracer Display trace for each execution of commands.
- --back-trace-limit n
- Display backtrace top n and tail n. The default
- value is 16.
- --verbose Show details
- --noverbose Don't show details
- -v, --version Print the version of irb
- -h, --help Print help
- -- Separate options of irb from the list of command-line args
-
-# vim:fileencoding=utf-8
+ --tracer Show stack trace for each command.
+ --back-trace-limit n[=16]
+ Display backtrace top n and bottom n.
+ --verbose Show details.
+ --noverbose Don't show details.
+ -v, --version Print the version of irb.
+ -h, --help Print help.
+ -- Separate options of irb from the list of command-line args.
diff --git a/lib/irb/lc/ja/encoding_aliases.rb b/lib/irb/lc/ja/encoding_aliases.rb
deleted file mode 100644
index 08180c3ec2..0000000000
--- a/lib/irb/lc/ja/encoding_aliases.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: false
-module IRB
- # :stopdoc:
-
- class Locale
- @@legacy_encoding_alias_map = {
- 'ujis' => Encoding::EUC_JP,
- 'euc' => Encoding::EUC_JP
- }.freeze
- end
-
- # :startdoc:
-end
diff --git a/lib/irb/lc/ja/error.rb b/lib/irb/lc/ja/error.rb
index d7c181c02e..9e2e5b8870 100644
--- a/lib/irb/lc/ja/error.rb
+++ b/lib/irb/lc/ja/error.rb
@@ -1,14 +1,8 @@
-# -*- coding: utf-8 -*-
-# frozen_string_literal: false
+# frozen_string_literal: true
+#
# irb/lc/ja/error.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB
# :stopdoc:
@@ -18,11 +12,6 @@ module IRB
super("スイッチ(#{val})が分りません")
end
end
- class NotImplementedError < StandardError
- def initialize(val)
- super("`#{val}'の定義が必要です")
- end
- end
class CantReturnToNormalMode < StandardError
def initialize
super("Normalモードに戻れません.")
@@ -48,11 +37,6 @@ module IRB
super("そのようなジョブ(#{val})はありません.")
end
end
- class CantShiftToMultiIrbMode < StandardError
- def initialize
- super("multi-irb modeに移れません.")
- end
- end
class CantChangeBinding < StandardError
def initialize(val)
super("バインディング(#{val})に変更できません.")
@@ -63,11 +47,6 @@ module IRB
super("プロンプトモード(#{val})は定義されていません.")
end
end
- class IllegalRCGenerator < StandardError
- def initialize
- super("RC_NAME_GENERATORが正しく定義されていません.")
- end
- end
# :startdoc:
end
diff --git a/lib/irb/lc/ja/help-message b/lib/irb/lc/ja/help-message
index 238535afb7..99f4449b3b 100644
--- a/lib/irb/lc/ja/help-message
+++ b/lib/irb/lc/ja/help-message
@@ -1,13 +1,3 @@
-# -*- coding: utf-8 -*-
-# irb/lc/ja/help-message.rb -
-# $Release Version: 0.9.6$
-# $Revision$
-# by Keiju ISHITSUKA(keiju@ruby-lang.org)
-#
-# --
-#
-#
-#
Usage: irb.rb [options] [programfile] [arguments]
-f ~/.irbrc を読み込まない.
-d $DEBUG をtrueにする(ruby -d と同じ)
@@ -19,10 +9,18 @@ Usage: irb.rb [options] [programfile] [arguments]
-W[level=2] ruby -W と同じ.
--context-mode n 新しいワークスペースを作成した時に関連する Binding
オブジェクトの作成方法を 0 から 3 のいずれかに設定する.
+ --extra-doc-dir 指定したディレクトリのドキュメントを追加で読み込む.
--echo 実行結果を表示する(デフォルト).
--noecho 実行結果を表示しない.
+ --echo-on-assignment
+ 代入結果を表示する.
+ --noecho-on-assignment
+ 代入結果を表示しない.
+ --truncate-echo-on-assignment
+ truncateされた代入結果を表示する(デフォルト).
--inspect 結果出力にinspectを用いる.
--noinspect 結果出力にinspectを用いない.
+ --no-pager ページャを使用しない.
--multiline マルチラインエディタを利用する.
--nomultiline マルチラインエディタを利用しない.
--singleline シングルラインエディタを利用する.
@@ -31,6 +29,9 @@ Usage: irb.rb [options] [programfile] [arguments]
--nocolorize 色付けを利用しない.
--autocomplete オートコンプリートを利用する.
--noautocomplete オートコンプリートを利用しない.
+ --regexp-completor
+ 補完に正規表現を利用する.
+ --type-completor 補完に型情報を利用する.
--prompt prompt-mode/--prompt-mode prompt-mode
プロンプトモードを切替えます. 現在定義されているプ
ロンプトモードは, default, simple, xmp, inf-rubyが
@@ -41,6 +42,8 @@ Usage: irb.rb [options] [programfile] [arguments]
--sample-book-mode/--simple-prompt
非常にシンプルなプロンプトを用いるモードです.
--noprompt プロンプト表示を行なわない.
+ --script スクリプトモード(最初の引数をスクリプトファイルとして扱う、デフォルト)
+ --noscript 引数をargvとして扱う.
--single-irb irb 中で self を実行して得られるオブジェクトをサ
ブ irb と共有する.
--tracer コマンド実行時にトレースを行なう.
@@ -53,5 +56,3 @@ Usage: irb.rb [options] [programfile] [arguments]
-v, --version irbのバージョンを表示する.
-h, --help irb のヘルプを表示する.
-- 以降のコマンドライン引数をオプションとして扱わない.
-
-# vim:fileencoding=utf-8
diff --git a/lib/irb/locale.rb b/lib/irb/locale.rb
index bb44b41002..2abcc7354b 100644
--- a/lib/irb/locale.rb
+++ b/lib/irb/locale.rb
@@ -1,14 +1,9 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/locale.rb - internationalization module
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
+
module IRB # :nodoc:
class Locale
@@ -20,7 +15,11 @@ module IRB # :nodoc:
]x
LOCALE_DIR = "/lc/"
- @@legacy_encoding_alias_map = {}.freeze
+ LEGACY_ENCODING_ALIAS_MAP = {
+ 'ujis' => Encoding::EUC_JP,
+ 'euc' => Encoding::EUC_JP
+ }
+
@@loaded = []
def initialize(locale = nil)
@@ -31,11 +30,11 @@ module IRB # :nodoc:
@lang, @territory, @encoding_name, @modifier = m[:language], m[:territory], m[:codeset], m[:modifier]
if @encoding_name
- begin load 'irb/encoding_aliases.rb'; rescue LoadError; end
- if @encoding = @@legacy_encoding_alias_map[@encoding_name]
+ if @encoding = LEGACY_ENCODING_ALIAS_MAP[@encoding_name]
warn(("%s is obsolete. use %s" % ["#{@lang}_#{@territory}.#{@encoding_name}", "#{@lang}_#{@territory}.#{@encoding.name}"]), uplevel: 1)
+ else
+ @encoding = Encoding.find(@encoding_name) rescue nil
end
- @encoding = Encoding.find(@encoding_name) rescue nil
end
end
@encoding ||= (Encoding.find('locale') rescue Encoding::ASCII_8BIT)
@@ -83,46 +82,19 @@ module IRB # :nodoc:
super(*ary)
end
- def require(file, priv = nil)
- rex = Regexp.new("lc/#{Regexp.quote(file)}\.(so|o|sl|rb)?")
- return false if $".find{|f| f =~ rex}
-
- case file
- when /\.rb$/
- begin
- load(file, priv)
- $".push file
- return true
- rescue LoadError
- end
- when /\.(so|o|sl)$/
- return super
- end
-
- begin
- load(f = file + ".rb")
- $".push f #"
- return true
- rescue LoadError
- return ruby_require(file)
- end
- end
-
- alias toplevel_load load
-
- def load(file, priv=nil)
+ def load(file)
found = find(file)
if found
unless @@loaded.include?(found)
@@loaded << found # cache
- return real_load(found, priv)
+ Kernel.load(found)
end
else
raise LoadError, "No such file to load -- #{file}"
end
end
- def find(file , paths = $:)
+ def find(file, paths = $:)
dir = File.dirname(file)
dir = "" if dir == "."
base = File.basename(file)
@@ -134,16 +106,6 @@ module IRB # :nodoc:
end
end
- private
- def real_load(path, priv)
- src = MagicFile.open(path){|f| f.read}
- if priv
- eval("self", TOPLEVEL_BINDING).extend(Module.new {eval(src, nil, path)})
- else
- eval(src, TOPLEVEL_BINDING, path)
- end
- end
-
# @param paths load paths in which IRB find a localized file.
# @param dir directory
# @param file basename to be localized
diff --git a/lib/irb/magic-file.rb b/lib/irb/magic-file.rb
deleted file mode 100644
index 34e06d64b3..0000000000
--- a/lib/irb/magic-file.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: false
-module IRB
- class << (MagicFile = Object.new)
- # see parser_magic_comment in parse.y
- ENCODING_SPEC_RE = %r"coding\s*[=:]\s*([[:alnum:]\-_]+)"
-
- def open(path)
- io = File.open(path, 'rb')
- line = io.gets
- line = io.gets if line[0,2] == "#!"
- encoding = detect_encoding(line)
- internal_encoding = encoding
- encoding ||= IRB.default_src_encoding
- io.rewind
- io.set_encoding(encoding, internal_encoding)
-
- if block_given?
- begin
- return (yield io)
- ensure
- io.close
- end
- else
- return io
- end
- end
-
- private
- def detect_encoding(line)
- return unless line[0] == ?#
- line = line[1..-1]
- line = $1 if line[/-\*-\s*(.*?)\s*-*-$/]
- return nil unless ENCODING_SPEC_RE =~ line
- encoding = $1
- return encoding.sub(/-(?:mac|dos|unix)/i, '')
- end
- end
-end
diff --git a/lib/irb/nesting_parser.rb b/lib/irb/nesting_parser.rb
new file mode 100644
index 0000000000..5aa940cc28
--- /dev/null
+++ b/lib/irb/nesting_parser.rb
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+module IRB
+ module NestingParser
+ IGNORE_TOKENS = %i[on_sp on_ignored_nl on_comment on_embdoc_beg on_embdoc on_embdoc_end]
+
+ # Scan each token and call the given block with array of token and other information for parsing
+ def self.scan_opens(tokens)
+ opens = []
+ pending_heredocs = []
+ first_token_on_line = true
+ tokens.each do |t|
+ skip = false
+ last_tok, state, args = opens.last
+ case state
+ when :in_alias_undef
+ skip = t.event == :on_kw
+ when :in_unquoted_symbol
+ unless IGNORE_TOKENS.include?(t.event)
+ opens.pop
+ skip = true
+ end
+ when :in_lambda_head
+ opens.pop if t.event == :on_tlambeg || (t.event == :on_kw && t.tok == 'do')
+ when :in_method_head
+ unless IGNORE_TOKENS.include?(t.event)
+ next_args = []
+ body = nil
+ if args.include?(:receiver)
+ case t.event
+ when :on_lparen, :on_ivar, :on_gvar, :on_cvar
+ # def (receiver). | def @ivar. | def $gvar. | def @@cvar.
+ next_args << :dot
+ when :on_kw
+ case t.tok
+ when 'self', 'true', 'false', 'nil'
+ # def self(arg) | def self.
+ next_args.push(:arg, :dot)
+ else
+ # def if(arg)
+ skip = true
+ next_args << :arg
+ end
+ when :on_op, :on_backtick
+ # def +(arg)
+ skip = true
+ next_args << :arg
+ when :on_ident, :on_const
+ # def a(arg) | def a.
+ next_args.push(:arg, :dot)
+ end
+ end
+ if args.include?(:dot)
+ # def receiver.name
+ next_args << :name if t.event == :on_period || (t.event == :on_op && t.tok == '::')
+ end
+ if args.include?(:name)
+ if %i[on_ident on_const on_op on_kw on_backtick].include?(t.event)
+ # def name(arg) | def receiver.name(arg)
+ next_args << :arg
+ skip = true
+ end
+ end
+ if args.include?(:arg)
+ case t.event
+ when :on_nl, :on_semicolon
+ # def receiver.f;
+ body = :normal
+ when :on_lparen
+ # def receiver.f()
+ next_args << :eq
+ else
+ if t.event == :on_op && t.tok == '='
+ # def receiver.f =
+ body = :oneliner
+ else
+ # def receiver.f arg
+ next_args << :arg_without_paren
+ end
+ end
+ end
+ if args.include?(:eq)
+ if t.event == :on_op && t.tok == '='
+ body = :oneliner
+ else
+ body = :normal
+ end
+ end
+ if args.include?(:arg_without_paren)
+ if %i[on_semicolon on_nl].include?(t.event)
+ # def f a;
+ body = :normal
+ else
+ # def f a, b
+ next_args << :arg_without_paren
+ end
+ end
+ if body == :oneliner
+ opens.pop
+ elsif body
+ opens[-1] = [last_tok, nil]
+ else
+ opens[-1] = [last_tok, :in_method_head, next_args]
+ end
+ end
+ when :in_for_while_until_condition
+ if t.event == :on_semicolon || t.event == :on_nl || (t.event == :on_kw && t.tok == 'do')
+ skip = true if t.event == :on_kw && t.tok == 'do'
+ opens[-1] = [last_tok, nil]
+ end
+ end
+
+ unless skip
+ case t.event
+ when :on_kw
+ case t.tok
+ when 'begin', 'class', 'module', 'do', 'case'
+ opens << [t, nil]
+ when 'end'
+ opens.pop
+ when 'def'
+ opens << [t, :in_method_head, [:receiver, :name]]
+ when 'if', 'unless'
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
+ opens << [t, nil]
+ end
+ when 'while', 'until'
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
+ opens << [t, :in_for_while_until_condition]
+ end
+ when 'ensure', 'rescue'
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
+ opens.pop
+ opens << [t, nil]
+ end
+ when 'alias'
+ opens << [t, :in_alias_undef, 2]
+ when 'undef'
+ opens << [t, :in_alias_undef, 1]
+ when 'elsif', 'else', 'when'
+ opens.pop
+ opens << [t, nil]
+ when 'for'
+ opens << [t, :in_for_while_until_condition]
+ when 'in'
+ if last_tok&.event == :on_kw && %w[case in].include?(last_tok.tok) && first_token_on_line
+ opens.pop
+ opens << [t, nil]
+ end
+ end
+ when :on_tlambda
+ opens << [t, :in_lambda_head]
+ when :on_lparen, :on_lbracket, :on_lbrace, :on_tlambeg, :on_embexpr_beg, :on_embdoc_beg
+ opens << [t, nil]
+ when :on_rparen, :on_rbracket, :on_rbrace, :on_embexpr_end, :on_embdoc_end
+ opens.pop
+ when :on_heredoc_beg
+ pending_heredocs << t
+ when :on_heredoc_end
+ opens.pop
+ when :on_backtick
+ opens << [t, nil] if t.state.allbits?(Ripper::EXPR_BEG)
+ when :on_tstring_beg, :on_words_beg, :on_qwords_beg, :on_symbols_beg, :on_qsymbols_beg, :on_regexp_beg
+ opens << [t, nil]
+ when :on_tstring_end, :on_regexp_end, :on_label_end
+ opens.pop
+ when :on_symbeg
+ if t.tok == ':'
+ opens << [t, :in_unquoted_symbol]
+ else
+ opens << [t, nil]
+ end
+ end
+ end
+ if t.event == :on_nl || t.event == :on_semicolon
+ first_token_on_line = true
+ elsif t.event != :on_sp
+ first_token_on_line = false
+ end
+ if pending_heredocs.any? && t.tok.include?("\n")
+ pending_heredocs.reverse_each { |t| opens << [t, nil] }
+ pending_heredocs = []
+ end
+ if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end
+ tok, state, arg = opens.pop
+ opens << [tok, state, arg - 1] if arg >= 1
+ end
+ yield t, opens if block_given?
+ end
+ opens.map(&:first) + pending_heredocs.reverse
+ end
+
+ def self.open_tokens(tokens)
+ # scan_opens without block will return a list of open tokens at last token position
+ scan_opens(tokens)
+ end
+
+ # Calculates token information [line_tokens, prev_opens, next_opens, min_depth] for each line.
+ # Example code
+ # ["hello
+ # world"+(
+ # First line
+ # line_tokens: [[lbracket, '['], [tstring_beg, '"'], [tstring_content("hello\nworld"), "hello\n"]]
+ # prev_opens: []
+ # next_tokens: [lbracket, tstring_beg]
+ # min_depth: 0 (minimum at beginning of line)
+ # Second line
+ # line_tokens: [[tstring_content("hello\nworld"), "world"], [tstring_end, '"'], [op, '+'], [lparen, '(']]
+ # prev_opens: [lbracket, tstring_beg]
+ # next_tokens: [lbracket, lparen]
+ # min_depth: 1 (minimum just after tstring_end)
+ def self.parse_by_line(tokens)
+ line_tokens = []
+ prev_opens = []
+ min_depth = 0
+ output = []
+ last_opens = scan_opens(tokens) do |t, opens|
+ depth = t == opens.last&.first ? opens.size - 1 : opens.size
+ min_depth = depth if depth < min_depth
+ if t.tok.include?("\n")
+ t.tok.each_line do |line|
+ line_tokens << [t, line]
+ next if line[-1] != "\n"
+ next_opens = opens.map(&:first)
+ output << [line_tokens, prev_opens, next_opens, min_depth]
+ prev_opens = next_opens
+ min_depth = prev_opens.size
+ line_tokens = []
+ end
+ else
+ line_tokens << [t, t.tok]
+ end
+ end
+ output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any?
+ output
+ end
+ end
+end
diff --git a/lib/irb/notifier.rb b/lib/irb/notifier.rb
index d0e413dd68..dc1b9ef14b 100644
--- a/lib/irb/notifier.rb
+++ b/lib/irb/notifier.rb
@@ -1,14 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# notifier.rb - output methods used by irb
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
require_relative "output-method"
diff --git a/lib/irb/output-method.rb b/lib/irb/output-method.rb
index 3fda708cb0..69942f47a2 100644
--- a/lib/irb/output-method.rb
+++ b/lib/irb/output-method.rb
@@ -1,30 +1,18 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# output-method.rb - output methods used by irb
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
module IRB
# An abstract output class for IO in irb. This is mainly used internally by
# IRB::Notifier. You can define your own output method to use with Irb.new,
# or Context.new
class OutputMethod
- class NotImplementedError < StandardError
- def initialize(val)
- super("Need to define `#{val}'")
- end
- end
-
# Open this method to implement your own output method, raises a
# NotImplementedError if you don't define #print in your own class.
def print(*opts)
- raise NotImplementedError, "print"
+ raise NotImplementedError
end
# Prints the given +opts+, with a newline delimiter.
diff --git a/lib/irb/pager.rb b/lib/irb/pager.rb
new file mode 100644
index 0000000000..3391b32c66
--- /dev/null
+++ b/lib/irb/pager.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module IRB
+ # The implementation of this class is borrowed from RDoc's lib/rdoc/ri/driver.rb.
+ # Please do NOT use this class directly outside of IRB.
+ class Pager
+ PAGE_COMMANDS = [ENV['RI_PAGER'], ENV['PAGER'], 'less', 'more'].compact.uniq
+
+ class << self
+ def page_content(content, **options)
+ if content_exceeds_screen_height?(content)
+ page(**options) do |io|
+ io.puts content
+ end
+ else
+ $stdout.puts content
+ end
+ end
+
+ def page(retain_content: false)
+ if should_page? && pager = setup_pager(retain_content: retain_content)
+ begin
+ pid = pager.pid
+ yield pager
+ ensure
+ pager.close
+ end
+ else
+ yield $stdout
+ end
+ # When user presses Ctrl-C, IRB would raise `IRB::Abort`
+ # But since Pager is implemented by running paging commands like `less` in another process with `IO.popen`,
+ # the `IRB::Abort` exception only interrupts IRB's execution but doesn't affect the pager
+ # So to properly terminate the pager with Ctrl-C, we need to catch `IRB::Abort` and kill the pager process
+ rescue IRB::Abort
+ Process.kill("TERM", pid) if pid
+ nil
+ rescue Errno::EPIPE
+ end
+
+ private
+
+ def should_page?
+ IRB.conf[:USE_PAGER] && STDIN.tty? && (ENV.key?("TERM") && ENV["TERM"] != "dumb")
+ end
+
+ def content_exceeds_screen_height?(content)
+ screen_height, screen_width = begin
+ Reline.get_screen_size
+ rescue Errno::EINVAL
+ [24, 80]
+ end
+
+ pageable_height = screen_height - 3 # leave some space for previous and the current prompt
+
+ # If the content has more lines than the pageable height
+ content.lines.count > pageable_height ||
+ # Or if the content is a few long lines
+ pageable_height * screen_width < Reline::Unicode.calculate_width(content, true)
+ end
+
+ def setup_pager(retain_content:)
+ require 'shellwords'
+
+ PAGE_COMMANDS.each do |pager_cmd|
+ cmd = Shellwords.split(pager_cmd)
+ next if cmd.empty?
+
+ if cmd.first == 'less'
+ cmd << '-R' unless cmd.include?('-R')
+ cmd << '-X' if retain_content && !cmd.include?('-X')
+ end
+
+ begin
+ io = IO.popen(cmd, 'w')
+ rescue
+ next
+ end
+
+ if $? && $?.pid == io.pid && $?.exited? # pager didn't work
+ next
+ end
+
+ return io
+ end
+
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb
index 29862f5507..cfe36be83f 100644
--- a/lib/irb/ruby-lex.rb
+++ b/lib/irb/ruby-lex.rb
@@ -1,861 +1,474 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/ruby-lex.rb - ruby lexcal analyzer
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
require "ripper"
require "jruby" if RUBY_ENGINE == "jruby"
-
-# :stopdoc:
-class RubyLex
-
- class TerminateLineInput < StandardError
- def initialize
- super("Terminate Line Input")
- end
- end
-
- def initialize
- @exp_line_no = @line_no = 1
- @indent = 0
- @continue = false
- @line = ""
- @prompt = nil
- end
-
- def self.compile_with_errors_suppressed(code, line_no: 1)
- begin
- result = yield code, line_no
- rescue ArgumentError
- # Ruby can issue an error for the code if there is an
- # incomplete magic comment for encoding in it. Force an
- # expression with a new line before the code in this
- # case to prevent magic comment handling. To make sure
- # line numbers in the lexed code remain the same,
- # decrease the line number by one.
- code = ";\n#{code}"
- line_no -= 1
- result = yield code, line_no
- end
- result
- end
-
- # io functions
- def set_input(io, p = nil, context: nil, &block)
- @io = io
- if @io.respond_to?(:check_termination)
- @io.check_termination do |code|
- if Reline::IOGate.in_pasting?
- lex = RubyLex.new
- rest = lex.check_termination_in_prev_line(code, context: context)
- if rest
- Reline.delete_text
- rest.bytes.reverse_each do |c|
- Reline.ungetc(c)
- end
- true
- else
- false
- end
- else
- code.gsub!(/\s*\z/, '').concat("\n")
- ltype, indent, continue, code_block_open = check_state(code, context: context)
- if ltype or indent > 0 or continue or code_block_open
- false
- else
- true
- end
- end
+require_relative "nesting_parser"
+
+module IRB
+ # :stopdoc:
+ class RubyLex
+ ASSIGNMENT_NODE_TYPES = [
+ # Local, instance, global, class, constant, instance, and index assignment:
+ # "foo = bar",
+ # "@foo = bar",
+ # "$foo = bar",
+ # "@@foo = bar",
+ # "::Foo = bar",
+ # "a::Foo = bar",
+ # "Foo = bar"
+ # "foo.bar = 1"
+ # "foo[1] = bar"
+ :assign,
+
+ # Operation assignment:
+ # "foo += bar"
+ # "foo -= bar"
+ # "foo ||= bar"
+ # "foo &&= bar"
+ :opassign,
+
+ # Multiple assignment:
+ # "foo, bar = 1, 2
+ :massign,
+ ]
+
+ class TerminateLineInput < StandardError
+ def initialize
+ super("Terminate Line Input")
end
end
- if @io.respond_to?(:dynamic_prompt)
- @io.dynamic_prompt do |lines|
- lines << '' if lines.empty?
- result = []
- tokens = self.class.ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join, context: context)
- code = String.new
- partial_tokens = []
- unprocessed_tokens = []
- line_num_offset = 0
- tokens.each do |t|
- partial_tokens << t
- unprocessed_tokens << t
- if t.tok.include?("\n")
- t_str = t.tok
- t_str.each_line("\n") do |s|
- code << s << "\n"
- ltype, indent, continue, code_block_open = check_state(code, partial_tokens, context: context)
- result << @prompt.call(ltype, indent, continue || code_block_open, @line_no + line_num_offset)
- line_num_offset += 1
- end
- unprocessed_tokens = []
- else
- code << t.tok
- end
- end
- unless unprocessed_tokens.empty?
- ltype, indent, continue, code_block_open = check_state(code, unprocessed_tokens, context: context)
- result << @prompt.call(ltype, indent, continue || code_block_open, @line_no + line_num_offset)
- end
- result
+ def self.compile_with_errors_suppressed(code, line_no: 1)
+ begin
+ result = yield code, line_no
+ rescue ArgumentError
+ # Ruby can issue an error for the code if there is an
+ # incomplete magic comment for encoding in it. Force an
+ # expression with a new line before the code in this
+ # case to prevent magic comment handling. To make sure
+ # line numbers in the lexed code remain the same,
+ # decrease the line number by one.
+ code = ";\n#{code}"
+ line_no -= 1
+ result = yield code, line_no
end
+ result
end
- if p.respond_to?(:call)
- @input = p
- elsif block_given?
- @input = block
- else
- @input = Proc.new{@io.gets}
+ ERROR_TOKENS = [
+ :on_parse_error,
+ :compile_error,
+ :on_assign_error,
+ :on_alias_error,
+ :on_class_name_error,
+ :on_param_error
+ ]
+
+ def self.generate_local_variables_assign_code(local_variables)
+ "#{local_variables.join('=')}=nil;" unless local_variables.empty?
end
- end
- def set_prompt(p = nil, &block)
- p = block if block_given?
- if p.respond_to?(:call)
- @prompt = p
- else
- @prompt = Proc.new{print p}
+ # Some part of the code is not included in Ripper's token.
+ # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr.
+ # With interpolated tokens, tokens.map(&:tok).join will be equal to code.
+ def self.interpolate_ripper_ignored_tokens(code, tokens)
+ line_positions = [0]
+ code.lines.each do |line|
+ line_positions << line_positions.last + line.bytesize
+ end
+ prev_byte_pos = 0
+ interpolated = []
+ prev_line = 1
+ tokens.each do |t|
+ line, col = t.pos
+ byte_pos = line_positions[line - 1] + col
+ if prev_byte_pos < byte_pos
+ tok = code.byteslice(prev_byte_pos...byte_pos)
+ pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
+ interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
+ prev_line += tok.count("\n")
+ end
+ interpolated << t
+ prev_byte_pos = byte_pos + t.tok.bytesize
+ prev_line += t.tok.count("\n")
+ end
+ if prev_byte_pos < code.bytesize
+ tok = code.byteslice(prev_byte_pos..)
+ pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
+ interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
+ end
+ interpolated
end
- end
- ERROR_TOKENS = [
- :on_parse_error,
- :compile_error,
- :on_assign_error,
- :on_alias_error,
- :on_class_name_error,
- :on_param_error
- ]
-
- def self.ripper_lex_without_warning(code, context: nil)
- verbose, $VERBOSE = $VERBOSE, nil
- if context
- lvars = context&.workspace&.binding&.local_variables
- if lvars && !lvars.empty?
- code = "#{lvars.join('=')}=nil\n#{code}"
+ def self.ripper_lex_without_warning(code, local_variables: [])
+ verbose, $VERBOSE = $VERBOSE, nil
+ lvars_code = generate_local_variables_assign_code(local_variables)
+ original_code = code
+ if lvars_code
+ code = "#{lvars_code}\n#{code}"
line_no = 0
else
line_no = 1
end
- end
- tokens = nil
- compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
- lexer = Ripper::Lexer.new(inner_code, '-', line_no)
- if lexer.respond_to?(:scan) # Ruby 2.7+
+
+ compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
+ lexer = Ripper::Lexer.new(inner_code, '-', line_no)
tokens = []
- pos_to_index = {}
lexer.scan.each do |t|
next if t.pos.first == 0
- if pos_to_index.has_key?(t.pos)
- index = pos_to_index[t.pos]
- found_tk = tokens[index]
- if ERROR_TOKENS.include?(found_tk.event) && !ERROR_TOKENS.include?(t.event)
- tokens[index] = t
- end
+ prev_tk = tokens.last
+ position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize
+ if position_overlapped
+ tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event)
else
- pos_to_index[t.pos] = tokens.size
tokens << t
end
end
- else
- tokens = lexer.parse.reject { |it| it.pos.first == 0 }
- end
- end
- tokens
- ensure
- $VERBOSE = verbose
- end
-
- def find_prev_spaces(line_index)
- return 0 if @tokens.size == 0
- md = @tokens[0].tok.match(/(\A +)/)
- prev_spaces = md.nil? ? 0 : md[1].count(' ')
- line_count = 0
- @tokens.each_with_index do |t, i|
- if t.tok.include?("\n")
- line_count += t.tok.count("\n")
- if line_count >= line_index
- return prev_spaces
- end
- if (@tokens.size - 1) > i
- md = @tokens[i + 1].tok.match(/(\A +)/)
- prev_spaces = md.nil? ? 0 : md[1].count(' ')
- end
- end
- end
- prev_spaces
- end
-
- def set_auto_indent(context)
- if @io.respond_to?(:auto_indent) and context.auto_indent_mode
- @io.auto_indent do |lines, line_index, byte_pointer, is_newline|
- if is_newline
- @tokens = self.class.ripper_lex_without_warning(lines[0..line_index].join("\n"), context: context)
- prev_spaces = find_prev_spaces(line_index)
- depth_difference = check_newline_depth_difference
- depth_difference = 0 if depth_difference < 0
- prev_spaces + depth_difference * 2
- else
- code = line_index.zero? ? '' : lines[0..(line_index - 1)].map{ |l| l + "\n" }.join
- last_line = lines[line_index]&.byteslice(0, byte_pointer)
- code += last_line if last_line
- @tokens = self.class.ripper_lex_without_warning(code, context: context)
- corresponding_token_depth = check_corresponding_token_depth(lines, line_index)
- if corresponding_token_depth
- corresponding_token_depth
- else
- nil
- end
- end
+ interpolate_ripper_ignored_tokens(original_code, tokens)
end
+ ensure
+ $VERBOSE = verbose
end
- end
- def check_state(code, tokens = nil, context: nil)
- tokens = self.class.ripper_lex_without_warning(code, context: context) unless tokens
- ltype = process_literal_type(tokens)
- indent = process_nesting_level(tokens)
- continue = process_continue(tokens)
- code_block_open = check_code_block(code, tokens)
- [ltype, indent, continue, code_block_open]
- end
-
- def prompt
- if @prompt
- @prompt.call(@ltype, @indent, @continue, @line_no)
+ def check_code_state(code, local_variables:)
+ tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
+ opens = NestingParser.open_tokens(tokens)
+ [tokens, opens, code_terminated?(code, tokens, opens, local_variables: local_variables)]
end
- end
- def initialize_input
- @ltype = nil
- @indent = 0
- @continue = false
- @line = ""
- @exp_line_no = @line_no
- @code_block_open = false
- end
-
- def each_top_level_statement
- initialize_input
- catch(:TERM_INPUT) do
- loop do
- begin
- prompt
- unless l = lex
- throw :TERM_INPUT if @line == ''
- else
- @line_no += l.count("\n")
- if l == "\n"
- @exp_line_no += 1
- next
- end
- @line.concat l
- if @code_block_open or @ltype or @continue or @indent > 0
- next
- end
- end
- if @line != "\n"
- @line.force_encoding(@io.encoding)
- yield @line, @exp_line_no
- end
- raise TerminateLineInput if @io.eof?
- @line = ''
- @exp_line_no = @line_no
-
- @indent = 0
- rescue TerminateLineInput
- initialize_input
- prompt
- end
+ def code_terminated?(code, tokens, opens, local_variables:)
+ case check_code_syntax(code, local_variables: local_variables)
+ when :unrecoverable_error
+ true
+ when :recoverable_error
+ false
+ when :other_error
+ opens.empty? && !should_continue?(tokens)
+ when :valid
+ !should_continue?(tokens)
end
end
- end
-
- def lex
- line = @input.call
- if @io.respond_to?(:check_termination)
- return line # multiline
- end
- code = @line + (line.nil? ? '' : line)
- code.gsub!(/\s*\z/, '').concat("\n")
- @tokens = self.class.ripper_lex_without_warning(code)
- @continue = process_continue
- @code_block_open = check_code_block(code)
- @indent = process_nesting_level
- @ltype = process_literal_type
- line
- end
-
- def process_continue(tokens = @tokens)
- # last token is always newline
- if tokens.size >= 2 and tokens[-2].event == :on_regexp_end
- # end of regexp literal
- return false
- elsif tokens.size >= 2 and tokens[-2].event == :on_semicolon
- return false
- elsif tokens.size >= 2 and tokens[-2].event == :on_kw and ['begin', 'else', 'ensure'].include?(tokens[-2].tok)
- return false
- elsif !tokens.empty? and tokens.last.tok == "\\\n"
- return true
- elsif tokens.size >= 1 and tokens[-1].event == :on_heredoc_end # "EOH\n"
- return false
- elsif tokens.size >= 2 and defined?(Ripper::EXPR_BEG) and tokens[-2].state.anybits?(Ripper::EXPR_BEG | Ripper::EXPR_FNAME) and tokens[-2].tok !~ /\A\.\.\.?\z/
- # end of literal except for regexp
- # endless range at end of line is not a continue
- return true
- end
- false
- end
- def check_code_block(code, tokens = @tokens)
- return true if tokens.empty?
- if tokens.last.event == :on_heredoc_beg
- return true
- end
+ def assignment_expression?(code, local_variables:)
+ # Try to parse the code and check if the last of possibly multiple
+ # expressions is an assignment type.
- begin # check if parser error are available
+ # If the expression is invalid, Ripper.sexp should return nil which will
+ # result in false being returned. Any valid expression should return an
+ # s-expression where the second element of the top level array is an
+ # array of parsed expressions. The first element of each expression is the
+ # expression's type.
verbose, $VERBOSE = $VERBOSE, nil
- case RUBY_ENGINE
- when 'ruby'
- self.class.compile_with_errors_suppressed(code) do |inner_code, line_no|
- RubyVM::InstructionSequence.compile(inner_code, nil, nil, line_no)
- end
- when 'jruby'
- JRuby.compile_ir(code)
- else
- catch(:valid) do
- eval("BEGIN { throw :valid, true }\n#{code}")
- false
- end
- end
- rescue EncodingError
- # This is for a hash with invalid encoding symbol, {"\xAE": 1}
- rescue SyntaxError => e
- case e.message
- when /unterminated (?:string|regexp) meets end of file/
- # "unterminated regexp meets end of file"
- #
- # example:
- # /
- #
- # "unterminated string meets end of file"
- #
- # example:
- # '
- return true
- when /syntax error, unexpected end-of-input/
- # "syntax error, unexpected end-of-input, expecting keyword_end"
- #
- # example:
- # if true
- # hoge
- # if false
- # fuga
- # end
- return true
- when /syntax error, unexpected keyword_end/
- # "syntax error, unexpected keyword_end"
- #
- # example:
- # if (
- # end
- #
- # example:
- # end
- return false
- when /syntax error, unexpected '\.'/
- # "syntax error, unexpected '.'"
- #
- # example:
- # .
- return false
- when /unexpected tREGEXP_BEG/
- # "syntax error, unexpected tREGEXP_BEG, expecting keyword_do or '{' or '('"
- #
- # example:
- # method / f /
- return false
- end
+ code = "#{RubyLex.generate_local_variables_assign_code(local_variables) || 'nil;'}\n#{code}"
+ # Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
+ node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
+ ASSIGNMENT_NODE_TYPES.include?(node_type)
ensure
$VERBOSE = verbose
end
- if defined?(Ripper::EXPR_BEG)
- last_lex_state = tokens.last.state
- if last_lex_state.allbits?(Ripper::EXPR_BEG)
- return false
- elsif last_lex_state.allbits?(Ripper::EXPR_DOT)
- return true
- elsif last_lex_state.allbits?(Ripper::EXPR_CLASS)
- return true
- elsif last_lex_state.allbits?(Ripper::EXPR_FNAME)
- return true
- elsif last_lex_state.allbits?(Ripper::EXPR_VALUE)
- return true
- elsif last_lex_state.allbits?(Ripper::EXPR_ARG)
- return false
+ def should_continue?(tokens)
+ # Look at the last token and check if IRB need to continue reading next line.
+ # Example code that should continue: `a\` `a +` `a.`
+ # Trailing spaces, newline, comments are skipped
+ return true if tokens.last&.event == :on_sp && tokens.last.tok == "\\\n"
+
+ tokens.reverse_each do |token|
+ case token.event
+ when :on_sp, :on_nl, :on_ignored_nl, :on_comment, :on_embdoc_beg, :on_embdoc, :on_embdoc_end
+ # Skip
+ when :on_regexp_end, :on_heredoc_end, :on_semicolon
+ # State is EXPR_BEG but should not continue
+ return false
+ else
+ # Endless range should not continue
+ return false if token.event == :on_op && token.tok.match?(/\A\.\.\.?\z/)
+
+ # EXPR_DOT and most of the EXPR_BEG should continue
+ return token.state.anybits?(Ripper::EXPR_BEG | Ripper::EXPR_DOT)
+ end
end
+ false
end
- false
- end
+ def check_code_syntax(code, local_variables:)
+ lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
+ code = "#{lvars_code}\n#{code}"
- def process_nesting_level(tokens = @tokens)
- indent = 0
- in_oneliner_def = nil
- tokens.each_with_index { |t, index|
- # detecting one-liner method definition
- if in_oneliner_def.nil?
- if t.state.allbits?(Ripper::EXPR_ENDFN)
- in_oneliner_def = :ENDFN
- end
- else
- if t.state.allbits?(Ripper::EXPR_ENDFN)
- # continuing
- elsif t.state.allbits?(Ripper::EXPR_BEG)
- if t.tok == '='
- in_oneliner_def = :BODY
+ begin # check if parser error are available
+ verbose, $VERBOSE = $VERBOSE, nil
+ case RUBY_ENGINE
+ when 'ruby'
+ self.class.compile_with_errors_suppressed(code) do |inner_code, line_no|
+ RubyVM::InstructionSequence.compile(inner_code, nil, nil, line_no)
end
+ when 'jruby'
+ JRuby.compile_ir(code)
else
- if in_oneliner_def == :BODY
- # one-liner method definition
- indent -= 1
+ catch(:valid) do
+ eval("BEGIN { throw :valid, true }\n#{code}")
+ false
end
- in_oneliner_def = nil
end
- end
-
- case t.event
- when :on_lbracket, :on_lbrace, :on_lparen, :on_tlambeg
- indent += 1
- when :on_rbracket, :on_rbrace, :on_rparen
- indent -= 1
- when :on_kw
- next if index > 0 and tokens[index - 1].state.allbits?(Ripper::EXPR_FNAME)
- case t.tok
- when 'do'
- syntax_of_do = take_corresponding_syntax_to_kw_do(tokens, index)
- indent += 1 if syntax_of_do == :method_calling
- when 'def', 'case', 'for', 'begin', 'class', 'module'
- indent += 1
- when 'if', 'unless', 'while', 'until'
- # postfix if/unless/while/until must be Ripper::EXPR_LABEL
- indent += 1 unless t.state.allbits?(Ripper::EXPR_LABEL)
- when 'end'
- indent -= 1
+ rescue EncodingError
+ # This is for a hash with invalid encoding symbol, {"\xAE": 1}
+ :unrecoverable_error
+ rescue SyntaxError => e
+ case e.message
+ when /unterminated (?:string|regexp) meets end of file/
+ # "unterminated regexp meets end of file"
+ #
+ # example:
+ # /
+ #
+ # "unterminated string meets end of file"
+ #
+ # example:
+ # '
+ return :recoverable_error
+ when /syntax error, unexpected end-of-input/
+ # "syntax error, unexpected end-of-input, expecting keyword_end"
+ #
+ # example:
+ # if true
+ # hoge
+ # if false
+ # fuga
+ # end
+ return :recoverable_error
+ when /syntax error, unexpected keyword_end/
+ # "syntax error, unexpected keyword_end"
+ #
+ # example:
+ # if (
+ # end
+ #
+ # example:
+ # end
+ return :unrecoverable_error
+ when /syntax error, unexpected '\.'/
+ # "syntax error, unexpected '.'"
+ #
+ # example:
+ # .
+ return :unrecoverable_error
+ when /unexpected tREGEXP_BEG/
+ # "syntax error, unexpected tREGEXP_BEG, expecting keyword_do or '{' or '('"
+ #
+ # example:
+ # method / f /
+ return :unrecoverable_error
+ else
+ return :other_error
end
+ ensure
+ $VERBOSE = verbose
end
- # percent literals are not indented
- }
- indent
- end
+ :valid
+ end
- def is_method_calling?(tokens, index)
- tk = tokens[index]
- if tk.state.anybits?(Ripper::EXPR_CMDARG) and tk.event == :on_ident
- # The target method call to pass the block with "do".
- return true
- elsif tk.state.anybits?(Ripper::EXPR_ARG) and tk.event == :on_ident
- non_sp_index = tokens[0..(index - 1)].rindex{ |t| t.event != :on_sp }
- if non_sp_index
- prev_tk = tokens[non_sp_index]
- if prev_tk.state.anybits?(Ripper::EXPR_DOT) and prev_tk.event == :on_period
- # The target method call with receiver to pass the block with "do".
- return true
+ def calc_indent_level(opens)
+ indent_level = 0
+ opens.each_with_index do |t, index|
+ case t.event
+ when :on_heredoc_beg
+ if opens[index + 1]&.event != :on_heredoc_beg
+ if t.tok.match?(/^<<[~-]/)
+ indent_level += 1
+ else
+ indent_level = 0
+ end
+ end
+ when :on_tstring_beg, :on_regexp_beg, :on_symbeg, :on_backtick
+ # No indent: "", //, :"", ``
+ # Indent: %(), %r(), %i(), %x()
+ indent_level += 1 if t.tok.start_with? '%'
+ when :on_embdoc_beg
+ indent_level = 0
+ else
+ indent_level += 1 unless t.tok == 'alias' || t.tok == 'undef'
end
end
+ indent_level
end
- false
- end
- def take_corresponding_syntax_to_kw_do(tokens, index)
- syntax_of_do = nil
- # Finding a syntax corresponding to "do".
- index.downto(0) do |i|
- tk = tokens[i]
- # In "continue", the token isn't the corresponding syntax to "do".
- non_sp_index = tokens[0..(i - 1)].rindex{ |t| t.event != :on_sp }
- first_in_fomula = false
- if non_sp_index.nil?
- first_in_fomula = true
- elsif [:on_ignored_nl, :on_nl, :on_comment].include?(tokens[non_sp_index].event)
- first_in_fomula = true
- end
- if is_method_calling?(tokens, i)
- syntax_of_do = :method_calling
- break if first_in_fomula
- elsif tk.event == :on_kw && %w{while until for}.include?(tk.tok)
- # A loop syntax in front of "do" found.
- #
- # while cond do # also "until" or "for"
- # end
- #
- # This "do" doesn't increment indent because the loop syntax already
- # incremented.
- syntax_of_do = :loop_syntax
- break if first_in_fomula
- end
+ FREE_INDENT_TOKENS = %i[on_tstring_beg on_backtick on_regexp_beg on_symbeg]
+
+ def free_indent_token?(token)
+ FREE_INDENT_TOKENS.include?(token&.event)
end
- syntax_of_do
- end
- def is_the_in_correspond_to_a_for(tokens, index)
- syntax_of_in = nil
- # Finding a syntax corresponding to "do".
- index.downto(0) do |i|
- tk = tokens[i]
- # In "continue", the token isn't the corresponding syntax to "do".
- non_sp_index = tokens[0..(i - 1)].rindex{ |t| t.event != :on_sp }
- first_in_fomula = false
- if non_sp_index.nil?
- first_in_fomula = true
- elsif [:on_ignored_nl, :on_nl, :on_comment].include?(tokens[non_sp_index].event)
- first_in_fomula = true
- end
- if tk.event == :on_kw && tk.tok == 'for'
- # A loop syntax in front of "do" found.
- #
- # while cond do # also "until" or "for"
- # end
- #
- # This "do" doesn't increment indent because the loop syntax already
- # incremented.
- syntax_of_in = :for
+ # Calculates the difference of pasted code's indent and indent calculated from tokens
+ def indent_difference(lines, line_results, line_index)
+ loop do
+ _tokens, prev_opens, _next_opens, min_depth = line_results[line_index]
+ open_token = prev_opens.last
+ if !open_token || (open_token.event != :on_heredoc_beg && !free_indent_token?(open_token))
+ # If the leading whitespace is an indent, return the difference
+ indent_level = calc_indent_level(prev_opens.take(min_depth))
+ calculated_indent = 2 * indent_level
+ actual_indent = lines[line_index][/^ */].size
+ return actual_indent - calculated_indent
+ elsif open_token.event == :on_heredoc_beg && open_token.tok.match?(/^<<[^-~]/)
+ return 0
+ end
+ # If the leading whitespace is not an indent but part of a multiline token
+ # Calculate base_indent of the multiline token's beginning line
+ line_index = open_token.pos[0] - 1
end
- break if first_in_fomula
end
- syntax_of_in
- end
- def check_newline_depth_difference
- depth_difference = 0
- open_brace_on_line = 0
- in_oneliner_def = nil
- @tokens.each_with_index do |t, index|
- # detecting one-liner method definition
- if in_oneliner_def.nil?
- if t.state.allbits?(Ripper::EXPR_ENDFN)
- in_oneliner_def = :ENDFN
- end
+ def process_indent_level(tokens, lines, line_index, is_newline)
+ line_results = NestingParser.parse_by_line(tokens)
+ result = line_results[line_index]
+ if result
+ _tokens, prev_opens, next_opens, min_depth = result
else
- if t.state.allbits?(Ripper::EXPR_ENDFN)
- # continuing
- elsif t.state.allbits?(Ripper::EXPR_BEG)
- if t.tok == '='
- in_oneliner_def = :BODY
- end
- else
- if in_oneliner_def == :BODY
- # one-liner method definition
- depth_difference -= 1
- end
- in_oneliner_def = nil
- end
+ # When last line is empty
+ prev_opens = next_opens = line_results.last[2]
+ min_depth = next_opens.size
end
- case t.event
- when :on_ignored_nl, :on_nl, :on_comment
- if index != (@tokens.size - 1) and in_oneliner_def != :BODY
- depth_difference = 0
- open_brace_on_line = 0
- end
- next
- when :on_sp
- next
- end
+ # To correctly indent line like `end.map do`, we use shortest open tokens on each line for indent calculation.
+ # Shortest open tokens can be calculated by `opens.take(min_depth)`
+ indent = 2 * calc_indent_level(prev_opens.take(min_depth))
- case t.event
- when :on_lbracket, :on_lbrace, :on_lparen, :on_tlambeg
- depth_difference += 1
- open_brace_on_line += 1
- when :on_rbracket, :on_rbrace, :on_rparen
- depth_difference -= 1 if open_brace_on_line > 0
- when :on_kw
- next if index > 0 and @tokens[index - 1].state.allbits?(Ripper::EXPR_FNAME)
- case t.tok
- when 'do'
- syntax_of_do = take_corresponding_syntax_to_kw_do(@tokens, index)
- depth_difference += 1 if syntax_of_do == :method_calling
- when 'def', 'case', 'for', 'begin', 'class', 'module'
- depth_difference += 1
- when 'if', 'unless', 'while', 'until', 'rescue'
- # postfix if/unless/while/until/rescue must be Ripper::EXPR_LABEL
- unless t.state.allbits?(Ripper::EXPR_LABEL)
- depth_difference += 1
- end
- when 'else', 'elsif', 'ensure', 'when'
- depth_difference += 1
- when 'in'
- unless is_the_in_correspond_to_a_for(@tokens, index)
- depth_difference += 1
- end
- when 'end'
- depth_difference -= 1
- end
- end
- end
- depth_difference
- end
+ preserve_indent = lines[line_index - (is_newline ? 1 : 0)][/^ */].size
- def check_corresponding_token_depth(lines, line_index)
- corresponding_token_depth = nil
- is_first_spaces_of_line = true
- is_first_printable_of_line = true
- spaces_of_nest = []
- spaces_at_line_head = 0
- open_brace_on_line = 0
- in_oneliner_def = nil
-
- if heredoc_scope?
- return lines[line_index][/^ */].length
- end
+ prev_open_token = prev_opens.last
+ next_open_token = next_opens.last
- @tokens.each_with_index do |t, index|
- # detecting one-liner method definition
- if in_oneliner_def.nil?
- if t.state.allbits?(Ripper::EXPR_ENDFN)
- in_oneliner_def = :ENDFN
- end
+ # Calculates base indent for pasted code on the line where prev_open_token is located
+ # irb(main):001:1* if a # base_indent is 2, indent calculated from tokens is 0
+ # irb(main):002:1* if b # base_indent is 6, indent calculated from tokens is 2
+ # irb(main):003:0> c # base_indent is 6, indent calculated from tokens is 4
+ if prev_open_token
+ base_indent = [0, indent_difference(lines, line_results, prev_open_token.pos[0] - 1)].max
else
- if t.state.allbits?(Ripper::EXPR_ENDFN)
- # continuing
- elsif t.state.allbits?(Ripper::EXPR_BEG)
- if t.tok == '='
- in_oneliner_def = :BODY
- end
- else
- if in_oneliner_def == :BODY
- # one-liner method definition
- if is_first_printable_of_line
- corresponding_token_depth = spaces_of_nest.pop
- else
- spaces_of_nest.pop
- corresponding_token_depth = nil
- end
- end
- in_oneliner_def = nil
- end
+ base_indent = 0
end
- case t.event
- when :on_ignored_nl, :on_nl, :on_comment
- if in_oneliner_def != :BODY
- corresponding_token_depth = nil
- spaces_at_line_head = 0
- is_first_spaces_of_line = true
- is_first_printable_of_line = true
- open_brace_on_line = 0
+ if free_indent_token?(prev_open_token)
+ if is_newline && prev_open_token.pos[0] == line_index
+ # First newline inside free-indent token
+ base_indent + indent
+ else
+ # Accept any number of indent inside free-indent token
+ preserve_indent
end
- next
- when :on_sp
- spaces_at_line_head = t.tok.count(' ') if is_first_spaces_of_line
- is_first_spaces_of_line = false
- next
- end
-
- case t.event
- when :on_lbracket, :on_lbrace, :on_lparen, :on_tlambeg
- spaces_of_nest.push(spaces_at_line_head + open_brace_on_line * 2)
- open_brace_on_line += 1
- when :on_rbracket, :on_rbrace, :on_rparen
- if is_first_printable_of_line
- corresponding_token_depth = spaces_of_nest.pop
+ elsif prev_open_token&.event == :on_embdoc_beg || next_open_token&.event == :on_embdoc_beg
+ if prev_open_token&.event == next_open_token&.event
+ # Accept any number of indent inside embdoc content
+ preserve_indent
else
- spaces_of_nest.pop
- corresponding_token_depth = nil
+ # =begin or =end
+ 0
end
- open_brace_on_line -= 1
- when :on_kw
- next if index > 0 and @tokens[index - 1].state.allbits?(Ripper::EXPR_FNAME)
- case t.tok
- when 'do'
- syntax_of_do = take_corresponding_syntax_to_kw_do(@tokens, index)
- if syntax_of_do == :method_calling
- spaces_of_nest.push(spaces_at_line_head)
- end
- when 'def', 'case', 'for', 'begin', 'class', 'module'
- spaces_of_nest.push(spaces_at_line_head)
- when 'rescue'
- unless t.state.allbits?(Ripper::EXPR_LABEL)
- corresponding_token_depth = spaces_of_nest.last
- end
- when 'if', 'unless', 'while', 'until'
- # postfix if/unless/while/until must be Ripper::EXPR_LABEL
- unless t.state.allbits?(Ripper::EXPR_LABEL)
- spaces_of_nest.push(spaces_at_line_head)
- end
- when 'else', 'elsif', 'ensure', 'when'
- corresponding_token_depth = spaces_of_nest.last
- when 'in'
- if in_keyword_case_scope?
- corresponding_token_depth = spaces_of_nest.last
- end
- when 'end'
- if is_first_printable_of_line
- corresponding_token_depth = spaces_of_nest.pop
+ elsif prev_open_token&.event == :on_heredoc_beg
+ tok = prev_open_token.tok
+ if prev_opens.size <= next_opens.size
+ if is_newline && lines[line_index].empty? && line_results[line_index - 1][1].last != next_open_token
+ # First line in heredoc
+ tok.match?(/^<<[-~]/) ? base_indent + indent : indent
+ elsif tok.match?(/^<<~/)
+ # Accept extra indent spaces inside `<<~` heredoc
+ [base_indent + indent, preserve_indent].max
else
- spaces_of_nest.pop
- corresponding_token_depth = nil
+ # Accept any number of indent inside other heredoc
+ preserve_indent
end
+ else
+ # Heredoc close
+ prev_line_indent_level = calc_indent_level(prev_opens)
+ tok.match?(/^<<[~-]/) ? base_indent + 2 * (prev_line_indent_level - 1) : 0
end
+ else
+ base_indent + indent
end
- is_first_spaces_of_line = false
- is_first_printable_of_line = false
end
- corresponding_token_depth
- end
- def check_string_literal(tokens)
- i = 0
- start_token = []
- end_type = []
- while i < tokens.size
- t = tokens[i]
- case t.event
- when *end_type.last
- start_token.pop
- end_type.pop
- when :on_tstring_beg
- start_token << t
- end_type << [:on_tstring_end, :on_label_end]
- when :on_regexp_beg
- start_token << t
- end_type << :on_regexp_end
- when :on_symbeg
- acceptable_single_tokens = %i{on_ident on_const on_op on_cvar on_ivar on_gvar on_kw on_int on_backtick}
- if (i + 1) < tokens.size
- if acceptable_single_tokens.all?{ |st| tokens[i + 1].event != st }
- start_token << t
- end_type << :on_tstring_end
- else
- i += 1
- end
- end
- when :on_backtick
- start_token << t
- end_type << :on_tstring_end
- when :on_qwords_beg, :on_words_beg, :on_qsymbols_beg, :on_symbols_beg
- start_token << t
- end_type << :on_tstring_end
- when :on_heredoc_beg
- start_token << t
- end_type << :on_heredoc_end
- end
- i += 1
- end
- start_token.last.nil? ? nil : start_token.last
- end
+ LTYPE_TOKENS = %i[
+ on_heredoc_beg on_tstring_beg
+ on_regexp_beg on_symbeg on_backtick
+ on_symbols_beg on_qsymbols_beg
+ on_words_beg on_qwords_beg
+ ]
- def process_literal_type(tokens = @tokens)
- start_token = check_string_literal(tokens)
- return nil if start_token == ""
-
- case start_token&.event
- when :on_tstring_beg
- case start_token&.tok
- when ?" then ?"
- when /^%.$/ then ?"
- when /^%Q.$/ then ?"
- when ?' then ?'
- when /^%q.$/ then ?'
- end
- when :on_regexp_beg then ?/
- when :on_symbeg then ?:
- when :on_backtick then ?`
- when :on_qwords_beg then ?]
- when :on_words_beg then ?]
- when :on_qsymbols_beg then ?]
- when :on_symbols_beg then ?]
- when :on_heredoc_beg
- start_token&.tok =~ /<<[-~]?(['"`])[_a-zA-Z0-9]+\1/
- case $1
- when ?" then ?"
- when ?' then ?'
- when ?` then ?`
- else ?"
+ def ltype_from_open_tokens(opens)
+ start_token = opens.reverse_each.find do |tok|
+ LTYPE_TOKENS.include?(tok.event)
end
- else
- nil
- end
- end
+ return nil unless start_token
- def check_termination_in_prev_line(code, context: nil)
- tokens = self.class.ripper_lex_without_warning(code, context: context)
- past_first_newline = false
- index = tokens.rindex do |t|
- # traverse first token before last line
- if past_first_newline
- if t.tok.include?("\n")
- true
+ case start_token&.event
+ when :on_tstring_beg
+ case start_token&.tok
+ when ?" then ?"
+ when /^%.$/ then ?"
+ when /^%Q.$/ then ?"
+ when ?' then ?'
+ when /^%q.$/ then ?'
end
- elsif t.tok.include?("\n")
- past_first_newline = true
- false
+ when :on_regexp_beg then ?/
+ when :on_symbeg then ?:
+ when :on_backtick then ?`
+ when :on_qwords_beg then ?]
+ when :on_words_beg then ?]
+ when :on_qsymbols_beg then ?]
+ when :on_symbols_beg then ?]
+ when :on_heredoc_beg
+ start_token&.tok =~ /<<[-~]?(['"`])\w+\1/
+ $1 || ?"
else
- false
+ nil
end
end
- if index
- first_token = nil
- last_line_tokens = tokens[(index + 1)..(tokens.size - 1)]
- last_line_tokens.each do |t|
- unless [:on_sp, :on_ignored_sp, :on_comment].include?(t.event)
- first_token = t
- break
- end
- end
-
- if first_token.nil?
- return false
- elsif first_token && first_token.state == Ripper::EXPR_DOT
- return false
- else
- tokens_without_last_line = tokens[0..index]
- ltype = process_literal_type(tokens_without_last_line)
- indent = process_nesting_level(tokens_without_last_line)
- continue = process_continue(tokens_without_last_line)
- code_block_open = check_code_block(tokens_without_last_line.map(&:tok).join(''), tokens_without_last_line)
- if ltype or indent > 0 or continue or code_block_open
- return false
+ def check_termination_in_prev_line(code, local_variables:)
+ tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
+ past_first_newline = false
+ index = tokens.rindex do |t|
+ # traverse first token before last line
+ if past_first_newline
+ if t.tok.include?("\n")
+ true
+ end
+ elsif t.tok.include?("\n")
+ past_first_newline = true
+ false
else
- return last_line_tokens.map(&:tok).join('')
+ false
end
end
- end
- false
- end
-
- private
- def heredoc_scope?
- heredoc_tokens = @tokens.select { |t| [:on_heredoc_beg, :on_heredoc_end].include?(t.event) }
- heredoc_tokens[-1]&.event == :on_heredoc_beg
- end
+ if index
+ first_token = nil
+ last_line_tokens = tokens[(index + 1)..(tokens.size - 1)]
+ last_line_tokens.each do |t|
+ unless [:on_sp, :on_ignored_sp, :on_comment].include?(t.event)
+ first_token = t
+ break
+ end
+ end
- def in_keyword_case_scope?
- kw_tokens = @tokens.select { |t| t.event == :on_kw && ['case', 'for', 'end'].include?(t.tok) }
- counter = 0
- kw_tokens.reverse.each do |t|
- if t.tok == 'case'
- return true if counter.zero?
- counter += 1
- elsif t.tok == 'for'
- counter += 1
- elsif t.tok == 'end'
- counter -= 1
+ if first_token && first_token.state != Ripper::EXPR_DOT
+ tokens_without_last_line = tokens[0..index]
+ code_without_last_line = tokens_without_last_line.map(&:tok).join
+ opens_without_last_line = NestingParser.open_tokens(tokens_without_last_line)
+ if code_terminated?(code_without_last_line, tokens_without_last_line, opens_without_last_line, local_variables: local_variables)
+ return last_line_tokens.map(&:tok).join
+ end
+ end
end
+ false
end
- false
end
+ # :startdoc:
end
-# :startdoc:
+
+RubyLex = IRB::RubyLex
+Object.deprecate_constant(:RubyLex)
diff --git a/lib/irb/ruby_logo.aa b/lib/irb/ruby_logo.aa
index a34a3e2f28..61fe22c94a 100644
--- a/lib/irb/ruby_logo.aa
+++ b/lib/irb/ruby_logo.aa
@@ -1,3 +1,4 @@
+TYPE: LARGE
-+smJYYN?mm-
HB"BBYT TQg NggT
@@ -35,3 +36,45 @@
m7 NW H N HSVO1z=?11-
NgTH bB kH WBHWWHBHWmQgg&gggggNNN
NNggggggNN
+TYPE: ASCII
+ ,,,;;;;''''';;;'';,
+ ,,;'' ';;,;;; ',
+ ,,'' ;;'';'''';;;;;;
+ ,;' ;; ',, ;
+ ,;' ,;' ';, ;
+ ;' ,;; ',,,;
+ ,' ,;;,,,,,,,,,,,;;;;
+ ;' ;;';;;; ,;;
+ ;' ,;' ;; '',, ,;;;
+ ;; ,;' ; '';, ,; ;'
+;; ,;;' ;; ;; ;;
+;;, ,,;;' ; ;'; ;;
+;';;,,,,;;;;;;;,,, ;; ,' ; ;;
+; ;;''' ,;'; ''';,,, ; ,;' ;;;;
+;;;;, ; '; ''';;;' ';;;
+;'; ;, ;' '; ,;' ', ;;;
+;;; ; ,; '; ,,' ',, ;;
+;;; '; ;' ';,,'' ';,;;
+ '; ';,; ,,;''''''''';;;;;;,,;;;
+ ';,,;;,,;;;;;;;;;;''''''''''''''
+TYPE: UNICODE
+ ⣀⣤⣴⣾⣿⣿⣿⡛⠛⠛⠛⠛⣻⣿⠿⠛⠛⠶⣤⡀
+ ⣀⣴⠾⠛⠉⠁ ⠙⣿⣶⣤⣶⣟⣉ ⠈⠻⣦
+ ⣀⣴⠟⠋ ⢸⣿⠟⠻⣯⡙⠛⠛⠛⠶⠶⠶⢶⣽⣇
+ ⣠⡾⠋⠁ ⣾⡿ ⠈⠛⢦⣄ ⣿
+ ⣠⡾⠋ ⣰⣿⠃ ⠙⠷⣤⡀ ⣿
+ ⢀⡾⠋ ⣰⣿⡏ ⠈⠻⣦⣄⢠⣿
+ ⣰⠟⠁ ⣴⣿⣿⣁⣀⣠⣤⣤⣤⣤⣤⣤⣤⣴⠶⠿⣿⡏
+ ⣼⠏ ⢀⣾⣿⠟⣿⠿⣯⣍⠁ ⣰⣿⡇
+ ⢀⣼⠋ ⢀⣴⣿⠟⠁ ⢸⡇ ⠙⠻⢦⣄⡀ ⢠⡿⣿⡇
+⢀⣾⡏ ⢀⣴⣿⠟⠁ ⣿ ⠉⠻⢶⣄⡀⣰⡟ ⣿⠃
+⣾⣿⠁ ⣠⣶⡿⠋⠁ ⢹⡇ ⠈⣿⡏ ⢸⣿
+⣿⣿⡆ ⢀⣠⣴⣿⡿⠋ ⠈⣿ ⢀⡾⠋⣿ ⢸⣿
+⣿⠸⣿⣶⣤⣤⣤⣤⣶⣾⠿⠿⣿⣿⠶⣤⣤⣀⡀ ⢹⡇ ⣴⠟⠁ ⣿⡀⢸⣿
+⣿⢀⣿⣟⠛⠋⠉⠁ ⢰⡟⠹⣧ ⠈⠉⠛⠻⠶⢦⣤⣀⡀ ⠈⣿ ⣠⡾⠃ ⢸⡇⢸⡇
+⣿⣾⣿⢿⡄ ⣿⠁ ⠘⣧ ⠉⠙⠛⠷⣿⣿⡋ ⠸⣇⣸⡇
+⣿⠃⣿⠈⢿⡄ ⣸⠇ ⠘⣧ ⢀⣤⠾⠋⠈⠻⣦⡀ ⣿⣿⡇
+⣿⢸⡏ ⠈⣷⡀ ⢠⡿ ⠘⣧⡀ ⣠⡴⠟⠁ ⠈⠻⣦⣀ ⢿⣿⠁
+⢻⣾⡇ ⠘⣷ ⣼⠃ ⠘⣷⣠⣴⠟⠋ ⠙⢷⣄⢸⣿
+ ⠻⣧⡀ ⠘⣧⣰⡏ ⢀⣠⣤⠶⠛⠉⠛⠛⠛⠛⠛⠛⠻⢶⣶⣶⣶⣶⣶⣤⣤⣽⣿⣿
+ ⠈⠛⠷⢦⣤⣽⣿⣥⣤⣶⣶⡿⠿⠿⠶⠶⠶⠶⠾⠛⠛⠛⠛⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠁
diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb
new file mode 100644
index 0000000000..5d7d729d19
--- /dev/null
+++ b/lib/irb/source_finder.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require_relative "ruby-lex"
+
+module IRB
+ class SourceFinder
+ class EvaluationError < StandardError; end
+
+ class Source
+ attr_reader :file, :line
+ def initialize(file, line, ast_source = nil)
+ @file = file
+ @line = line
+ @ast_source = ast_source
+ end
+
+ def file_exist?
+ File.exist?(@file)
+ end
+
+ def binary_file?
+ # If the line is zero, it means that the target's source is probably in a binary file.
+ @line.zero?
+ end
+
+ def file_content
+ @file_content ||= File.read(@file)
+ end
+
+ def colorized_content
+ if !binary_file? && file_exist?
+ end_line = find_end
+ # To correctly colorize, we need to colorize full content and extract the relevant lines.
+ colored = IRB::Color.colorize_code(file_content)
+ colored.lines[@line - 1...end_line].join
+ elsif @ast_source
+ IRB::Color.colorize_code(@ast_source)
+ end
+ end
+
+ private
+
+ def find_end
+ lex = RubyLex.new
+ code = file_content
+ lines = code.lines[(@line - 1)..-1]
+ tokens = RubyLex.ripper_lex_without_warning(lines.join)
+ prev_tokens = []
+
+ # chunk with line number
+ tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
+ code = lines[0..lnum].join
+ prev_tokens.concat chunk
+ continue = lex.should_continue?(prev_tokens)
+ syntax = lex.check_code_syntax(code, local_variables: [])
+ if !continue && syntax == :valid
+ return @line + lnum
+ end
+ end
+ @line
+ end
+ end
+
+ private_constant :Source
+
+ def initialize(irb_context)
+ @irb_context = irb_context
+ end
+
+ def find_source(signature, super_level = 0)
+ case signature
+ when /\A(::)?[A-Z]\w*(::[A-Z]\w*)*\z/ # ConstName, ::ConstName, ConstPath::ConstName
+ eval_receiver_or_owner(signature) # trigger autoload
+ *parts, name = signature.split('::', -1)
+ base =
+ if parts.empty? # ConstName
+ find_const_owner(name)
+ elsif parts == [''] # ::ConstName
+ Object
+ else # ConstPath::ConstName
+ eval_receiver_or_owner(parts.join('::'))
+ end
+ file, line = base.const_source_location(name)
+ when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method
+ owner = eval_receiver_or_owner(Regexp.last_match[:owner])
+ method = Regexp.last_match[:method]
+ return unless owner.respond_to?(:instance_method)
+ method = method_target(owner, super_level, method, "owner")
+ file, line = method&.source_location
+ when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method
+ receiver = eval_receiver_or_owner(Regexp.last_match[:receiver] || 'self')
+ method = Regexp.last_match[:method]
+ return unless receiver.respond_to?(method, true)
+ method = method_target(receiver, super_level, method, "receiver")
+ file, line = method&.source_location
+ end
+ return unless file && line
+
+ if File.exist?(file)
+ Source.new(file, line)
+ elsif method
+ # Method defined with eval, probably in IRB session
+ source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil
+ Source.new(file, line, source)
+ end
+ rescue EvaluationError
+ nil
+ end
+
+ private
+
+ def method_target(owner_receiver, super_level, method, type)
+ case type
+ when "owner"
+ target_method = owner_receiver.instance_method(method)
+ when "receiver"
+ target_method = owner_receiver.method(method)
+ end
+ super_level.times do |s|
+ target_method = target_method.super_method if target_method
+ end
+ target_method
+ rescue NameError
+ nil
+ end
+
+ def eval_receiver_or_owner(code)
+ context_binding = @irb_context.workspace.binding
+ eval(code, context_binding)
+ rescue NameError
+ raise EvaluationError
+ end
+
+ def find_const_owner(name)
+ module_nesting = @irb_context.workspace.binding.eval('::Module.nesting')
+ module_nesting.find { |mod| mod.const_defined?(name, false) } || module_nesting.find { |mod| mod.const_defined?(name) } || Object
+ end
+ end
+end
diff --git a/lib/irb/src_encoding.rb b/lib/irb/src_encoding.rb
deleted file mode 100644
index 99aea2b43e..0000000000
--- a/lib/irb/src_encoding.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: false
-# DO NOT WRITE ANY MAGIC COMMENT HERE.
-module IRB
- def self.default_src_encoding
- return __ENCODING__
- end
-end
diff --git a/lib/irb/statement.rb b/lib/irb/statement.rb
new file mode 100644
index 0000000000..a3391c12a3
--- /dev/null
+++ b/lib/irb/statement.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module IRB
+ class Statement
+ attr_reader :code
+
+ def is_assignment?
+ raise NotImplementedError
+ end
+
+ def suppresses_echo?
+ raise NotImplementedError
+ end
+
+ def should_be_handled_by_debugger?
+ raise NotImplementedError
+ end
+
+ class EmptyInput < Statement
+ def is_assignment?
+ false
+ end
+
+ def suppresses_echo?
+ true
+ end
+
+ # Debugger takes empty input to repeat the last command
+ def should_be_handled_by_debugger?
+ true
+ end
+
+ def code
+ ""
+ end
+ end
+
+ class Expression < Statement
+ def initialize(code, is_assignment)
+ @code = code
+ @is_assignment = is_assignment
+ end
+
+ def suppresses_echo?
+ @code.match?(/;\s*\z/)
+ end
+
+ def should_be_handled_by_debugger?
+ true
+ end
+
+ def is_assignment?
+ @is_assignment
+ end
+ end
+
+ class Command < Statement
+ attr_reader :command_class, :arg
+
+ def initialize(original_code, command_class, arg)
+ @code = original_code
+ @command_class = command_class
+ @arg = arg
+ end
+
+ def is_assignment?
+ false
+ end
+
+ def suppresses_echo?
+ false
+ end
+
+ def should_be_handled_by_debugger?
+ require_relative 'command/debug'
+ IRB::Command::DebugCommand > @command_class
+ end
+ end
+ end
+end
diff --git a/lib/irb/version.rb b/lib/irb/version.rb
index 481d14ffd2..9a7b12766b 100644
--- a/lib/irb/version.rb
+++ b/lib/irb/version.rb
@@ -1,17 +1,11 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/version.rb - irb version definition file
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ishitsuka.com)
#
-# --
-#
-#
-#
module IRB # :nodoc:
- VERSION = "1.4.1"
+ VERSION = "1.12.0"
@RELEASE_VERSION = VERSION
- @LAST_UPDATE_DATE = "2021-12-25"
+ @LAST_UPDATE_DATE = "2024-03-06"
end
diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb
index e5ef52528a..d24d1cc38d 100644
--- a/lib/irb/workspace.rb
+++ b/lib/irb/workspace.rb
@@ -1,17 +1,13 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/workspace-binding.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
require "delegate"
+require_relative "helper_method"
+
IRB::TOPLEVEL_BINDING = binding
module IRB # :nodoc:
class WorkSpace
@@ -96,11 +92,11 @@ EOF
IRB.conf[:__MAIN__] = @main
@main.singleton_class.class_eval do
private
- define_method(:exit) do |*a, &b|
- # Do nothing, will be overridden
- end
define_method(:binding, Kernel.instance_method(:binding))
define_method(:local_variables, Kernel.instance_method(:local_variables))
+ # Define empty method to avoid delegator warning, will be overridden.
+ define_method(:exit) {|*a, &b| }
+ define_method(:exit!) {|*a, &b| }
end
@binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, *@binding.source_location)
end
@@ -114,8 +110,14 @@ EOF
# <code>IRB.conf[:__MAIN__]</code>
attr_reader :main
+ 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)
+ end
+
# Evaluate the given +statements+ within the context of this workspace.
- def evaluate(context, statements, file = __FILE__, line = __LINE__)
+ def evaluate(statements, file = __FILE__, line = __LINE__)
eval(statements, @binding, file, line)
end
@@ -128,6 +130,8 @@ EOF
end
# error message manipulator
+ # WARN: Rails patches this method to filter its own backtrace. Be cautious when changing it.
+ # See: https://github.com/rails/rails/blob/main/railties/lib/rails/commands/console/console_command.rb#L8:~:text=def,filter_backtrace
def filter_backtrace(bt)
return nil if bt =~ /\/irb\/.*\.rb/
return nil if bt =~ /\/irb\.rb/
@@ -142,11 +146,7 @@ EOF
end
def code_around_binding
- if @binding.respond_to?(:source_location)
- file, pos = @binding.source_location
- else
- file, pos = @binding.eval('[__FILE__, __LINE__]')
- end
+ file, pos = @binding.source_location
if defined?(::SCRIPT_LINES__[file]) && lines = ::SCRIPT_LINES__[file]
code = ::SCRIPT_LINES__[file].join('')
@@ -173,8 +173,17 @@ EOF
"\nFrom: #{file} @ line #{pos + 1} :\n\n#{body}#{Color.clear}\n"
end
+ end
- def IRB.delete_caller
+ module HelpersContainer
+ def self.install_helper_methods
+ HelperMethod.helper_methods.each do |name, helper_method_class|
+ define_method name do |*args, **opts, &block|
+ helper_method_class.instance.execute(*args, **opts, &block)
+ end unless method_defined?(name)
+ end
end
+
+ install_helper_methods
end
end
diff --git a/lib/irb/ws-for-case-2.rb b/lib/irb/ws-for-case-2.rb
index eb173fddca..03f42d73d9 100644
--- a/lib/irb/ws-for-case-2.rb
+++ b/lib/irb/ws-for-case-2.rb
@@ -1,14 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# irb/ws-for-case-2.rb -
-# $Release Version: 0.9.6$
-# $Revision$
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#
-# --
-#
-#
-#
while true
IRB::BINDING_QUEUE.push _ = binding
diff --git a/lib/irb/xmp.rb b/lib/irb/xmp.rb
index 88cbd88525..b1bc53283e 100644
--- a/lib/irb/xmp.rb
+++ b/lib/irb/xmp.rb
@@ -1,14 +1,8 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# xmp.rb - irb version of gotoken xmp
-# $Release Version: 0.9$
-# $Revision$
# by Keiju ISHITSUKA(Nippon Rational Inc.)
#
-# --
-#
-#
-#
require_relative "../irb"
require_relative "frame"
@@ -50,8 +44,8 @@ class XMP
# The top-level binding or, optional +bind+ parameter will be used when
# creating the workspace. See WorkSpace.new for more information.
#
- # This uses the +:XMP+ prompt mode, see IRB@Customizing+the+IRB+Prompt for
- # full detail.
+ # This uses the +:XMP+ prompt mode.
+ # See {Custom Prompts}[rdoc-ref:IRB@Custom+Prompts] for more information.
def initialize(bind = nil)
IRB.init_config(nil)