diff options
Diffstat (limited to 'lib/irb')
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) |