diff options
Diffstat (limited to 'lib/irb')
52 files changed, 4927 insertions, 3096 deletions
diff --git a/lib/irb/.document b/lib/irb/.document new file mode 100644 index 0000000000..3b0d6fa4ed --- /dev/null +++ b/lib/irb/.document @@ -0,0 +1 @@ +**/*.rb diff --git a/lib/irb/cmd/chws.rb b/lib/irb/cmd/chws.rb index 88585b778b..e9f257791c 100644 --- a/lib/irb/cmd/chws.rb +++ b/lib/irb/cmd/chws.rb @@ -1,33 +1,34 @@ +# frozen_string_literal: false # -# change-ws.rb - -# $Release Version: 0.9.5$ +# change-ws.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -require "irb/cmd/nop.rb" -require "irb/ext/change-ws.rb" +require_relative "nop" +require_relative "../ext/change-ws" +# :stopdoc: module IRB module ExtendCommand - class CurrentWorkingWorkspace<Nop + class CurrentWorkingWorkspace < Nop def execute(*obj) - irb_context.main + irb_context.main end end - class ChangeWorkspace<Nop + class ChangeWorkspace < Nop def execute(*obj) - irb_context.change_workspace(*obj) - irb_context.main + irb_context.change_workspace(*obj) + irb_context.main end end end end - +# :startdoc: diff --git a/lib/irb/cmd/fork.rb b/lib/irb/cmd/fork.rb index 2866b1373b..7566d10be0 100644 --- a/lib/irb/cmd/fork.rb +++ b/lib/irb/cmd/fork.rb @@ -1,39 +1,37 @@ +# frozen_string_literal: false # -# fork.rb - -# $Release Version: 0.9.5 $ +# fork.rb - +# $Release Version: 0.9.6 $ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # - -@RCS_ID='-$Id$-' +# +# :stopdoc: module IRB module ExtendCommand - class Fork<Nop - def execute(&block) - pid = send ExtendCommand.irb_original_method_name("fork") - unless pid - class<<self - alias_method :exit, ExtendCommand.irb_original_method_name('exit') - end - if iterator? - begin - yield - ensure - exit - end - end - end - pid + 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 end - - +# :startdoc: diff --git a/lib/irb/cmd/help.rb b/lib/irb/cmd/help.rb index 3e8d1388e0..d82e78fb57 100644 --- a/lib/irb/cmd/help.rb +++ b/lib/irb/cmd/help.rb @@ -1,34 +1,47 @@ +# frozen_string_literal: false # # help.rb - helper using ri -# $Release Version: 0.9.5$ +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # # -- # -# +# # -require 'rdoc/ri/ri_driver' +require_relative "nop" +# :stopdoc: module IRB module ExtendCommand - module Help - begin - @ri = RiDriver.new - rescue SystemExit + 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 self.execute(context, *names) + def execute(*names) + if names.empty? + Ri.interactive + return + end names.each do |name| begin - @ri.get_info_for(name.to_s) - rescue RiError + Ri.display_name(name.to_s) + rescue RDoc::RI::Error puts $!.message end end nil end + nil + ensure + execute(*names) end end end end +# :startdoc: diff --git a/lib/irb/cmd/info.rb b/lib/irb/cmd/info.rb new file mode 100644 index 0000000000..dd93352ff0 --- /dev/null +++ b/lib/irb/cmd/info.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: false + +require_relative "nop" + +# :stopdoc: +module IRB + 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 +end +# :startdoc: diff --git a/lib/irb/cmd/load.rb b/lib/irb/cmd/load.rb index cbc5d91d03..b6769a4124 100644 --- a/lib/irb/cmd/load.rb +++ b/lib/irb/cmd/load.rb @@ -1,67 +1,67 @@ +# frozen_string_literal: false # -# load.rb - -# $Release Version: 0.9.5$ +# load.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -require "irb/cmd/nop.rb" -require "irb/ext/loader" +require_relative "nop" +require_relative "../ext/loader" +# :stopdoc: module IRB module ExtendCommand - class Load<Nop + class Load < Nop include IrbLoader def execute(file_name, priv = nil) -# return ruby_load(file_name) unless IRB.conf[:USE_LOADER] - return irb_load(file_name, priv) + return irb_load(file_name, priv) end end - class Require<Nop + class Require < Nop include IrbLoader - + def execute(file_name) -# return ruby_require(file_name) unless IRB.conf[:USE_LOADER] - rex = Regexp.new("#{Regexp.quote(file_name)}(\.o|\.rb)?") - return false if $".find{|f| f =~ rex} + 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 - 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 + begin + irb_load(f = file_name + ".rb") + $".push f + return true + rescue LoadError + return ruby_require(file_name) + end end end - class Source<Nop + class Source < Nop include IrbLoader def execute(file_name) - source_file(file_name) + source_file(file_name) end end end end +# :startdoc: diff --git a/lib/irb/cmd/ls.rb b/lib/irb/cmd/ls.rb new file mode 100644 index 0000000000..cbbf96210e --- /dev/null +++ b/lib/irb/cmd/ls.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require "reline" +require_relative "nop" +require_relative "../color" + +# :stopdoc: +module IRB + module ExtendCommand + class Ls < Nop + def execute(*arg, grep: nil) + 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) + 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) + 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) + end + [mod, methods] + end.reverse + end + + class Output + MARGIN = " " + + def initialize(grep: nil) + @grep = grep + @line_width = screen_width - MARGIN.length # right padding + end + + def dump(name, strs) + strs = strs.grep(@grep) if @grep + strs = strs.sort + return if strs.empty? + + # Attempt a single line + print "#{Color.colorize(name, [:BOLD, :BLUE])}: " + if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length) + puts strs.join(MARGIN) + return + end + puts + + # Dump with the largest # of columns that fits on a line + cols = strs.size + until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1 + cols -= 1 + 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 + end + end + + private + + def fits_on_line?(strs, cols:, offset: 0) + width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1) + width <= @line_width - offset + end + + def col_widths(strs, cols:) + cols.times.map do |col| + (col...strs.size).step(cols).map do |i| + strs[i].length + end.max + end + end + + def screen_width + Reline.get_screen_size.last + rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN> + 80 + end + end + private_constant :Output + end + end +end +# :startdoc: diff --git a/lib/irb/cmd/measure.rb b/lib/irb/cmd/measure.rb new file mode 100644 index 0000000000..adea540e92 --- /dev/null +++ b/lib/irb/cmd/measure.rb @@ -0,0 +1,43 @@ +require_relative "nop" + +# :stopdoc: +module IRB + module ExtendCommand + class Measure < Nop + def initialize(*args) + super(*args) + end + + def execute(type = nil, arg = nil, &block) + # 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) + 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 + end + nil + end + end + end +end +# :startdoc: diff --git a/lib/irb/cmd/nop.rb b/lib/irb/cmd/nop.rb index aa553c959e..d6f7a611a6 100644 --- a/lib/irb/cmd/nop.rb +++ b/lib/irb/cmd/nop.rb @@ -1,39 +1,45 @@ +# frozen_string_literal: false # -# nop.rb - -# $Release Version: 0.9.5$ +# nop.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # +# +# :stopdoc: module IRB module ExtendCommand class Nop - - @RCS_ID='-$Id$-' - def self.execute(conf, *opts) - command = new(conf) - command.execute(*opts) + 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 + @irb_context = conf end attr_reader :irb_context def irb - @irb_context.irb + @irb_context.irb end def execute(*opts) - #nop + #nop end end end end - +# :startdoc: diff --git a/lib/irb/cmd/pushws.rb b/lib/irb/cmd/pushws.rb index eddaeae631..612157d8a0 100644 --- a/lib/irb/cmd/pushws.rb +++ b/lib/irb/cmd/pushws.rb @@ -1,39 +1,40 @@ +# frozen_string_literal: false # -# change-ws.rb - -# $Release Version: 0.9.5$ +# change-ws.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -require "irb/cmd/nop.rb" -require "irb/ext/workspaces.rb" +require_relative "nop" +require_relative "../ext/workspaces" +# :stopdoc: module IRB module ExtendCommand - class Workspaces<Nop + class Workspaces < Nop def execute(*obj) - irb_context.workspaces.collect{|ws| ws.main} + irb_context.workspaces.collect{|ws| ws.main} end end - class PushWorkspace<Workspaces + class PushWorkspace < Workspaces def execute(*obj) - irb_context.push_workspace(*obj) - super + irb_context.push_workspace(*obj) + super end end - class PopWorkspace<Workspaces + class PopWorkspace < Workspaces def execute(*obj) - irb_context.pop_workspace(*obj) - super + irb_context.pop_workspace(*obj) + super end end end end - +# :startdoc: diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb new file mode 100644 index 0000000000..8f203ef125 --- /dev/null +++ b/lib/irb/cmd/show_source.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require_relative "nop" +require_relative "../color" +require_relative "../ruby-lex" + +# :stopdoc: +module IRB + 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 +end +# :startdoc: diff --git a/lib/irb/cmd/subirb.rb b/lib/irb/cmd/subirb.rb index 79d654b172..1e18607d1a 100644 --- a/lib/irb/cmd/subirb.rb +++ b/lib/irb/cmd/subirb.rb @@ -1,43 +1,43 @@ -#!/usr/local/bin/ruby -# -# multi.rb - -# $Release Version: 0.9.5$ +# frozen_string_literal: false +# multi.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -require "irb/cmd/nop.rb" -require "irb/ext/multi-irb" +require_relative "nop" +require_relative "../ext/multi-irb" +# :stopdoc: module IRB module ExtendCommand - class IrbCommand<Nop + class IrbCommand < Nop def execute(*obj) - IRB.irb(nil, *obj) + IRB.irb(nil, *obj) end end - class Jobs<Nop + class Jobs < Nop def execute - IRB.JobManager + IRB.JobManager end end - class Foreground<Nop + class Foreground < Nop def execute(key) - IRB.JobManager.switch(key) + IRB.JobManager.switch(key) end end - class Kill<Nop + class Kill < Nop def execute(*keys) - IRB.JobManager.kill(*keys) + IRB.JobManager.kill(*keys) end end end end +# :startdoc: diff --git a/lib/irb/cmd/whereami.rb b/lib/irb/cmd/whereami.rb new file mode 100644 index 0000000000..b3def11b93 --- /dev/null +++ b/lib/irb/cmd/whereami.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "nop" + +# :stopdoc: +module IRB + module ExtendCommand + class Whereami < Nop + def execute(*) + code = irb_context.workspace.code_around_binding + if code + puts code + else + puts "The current context doesn't have code." + end + end + end + end +end +# :startdoc: diff --git a/lib/irb/color.rb b/lib/irb/color.rb new file mode 100644 index 0000000000..40e9e04c97 --- /dev/null +++ b/lib/irb/color.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true +require 'reline' +require 'ripper' +require 'irb/ruby-lex' + +module IRB # :nodoc: + module Color + CLEAR = 0 + BOLD = 1 + UNDERLINE = 4 + REVERSE = 7 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + + TOKEN_KEYWORDS = { + on_kw: ['nil', 'self', 'true', 'false', '__FILE__', '__LINE__', '__ENCODING__'], + on_const: ['ENV'], + } + private_constant :TOKEN_KEYWORDS + + # A constant of all-bit 1 to match any Ripper's state in #dispatch_seq + ALL = -1 + private_constant :ALL + + begin + # Following pry's colors where possible, but sometimes having a compromise like making + # backtick and regexp as red (string's color, because they're sharing tokens). + TOKEN_SEQ_EXPRS = { + on_CHAR: [[BLUE, BOLD], ALL], + on_backtick: [[RED, BOLD], ALL], + on_comment: [[BLUE, BOLD], ALL], + on_const: [[BLUE, BOLD, UNDERLINE], ALL], + on_embexpr_beg: [[RED], ALL], + on_embexpr_end: [[RED], ALL], + on_embvar: [[RED], ALL], + on_float: [[MAGENTA, BOLD], ALL], + on_gvar: [[GREEN, BOLD], ALL], + on_heredoc_beg: [[RED], ALL], + on_heredoc_end: [[RED], ALL], + on_ident: [[BLUE, BOLD], Ripper::EXPR_ENDFN], + on_imaginary: [[BLUE, BOLD], ALL], + on_int: [[BLUE, BOLD], ALL], + on_kw: [[GREEN], ALL], + on_label: [[MAGENTA], ALL], + on_label_end: [[RED, BOLD], ALL], + on_qsymbols_beg: [[RED, BOLD], ALL], + on_qwords_beg: [[RED, BOLD], ALL], + on_rational: [[BLUE, BOLD], ALL], + on_regexp_beg: [[RED, BOLD], ALL], + on_regexp_end: [[RED, BOLD], ALL], + on_symbeg: [[YELLOW], ALL], + on_symbols_beg: [[RED, BOLD], ALL], + on_tstring_beg: [[RED, BOLD], ALL], + on_tstring_content: [[RED], ALL], + on_tstring_end: [[RED, BOLD], ALL], + on_words_beg: [[RED, BOLD], ALL], + on_parse_error: [[RED, REVERSE], ALL], + compile_error: [[RED, REVERSE], ALL], + on_assign_error: [[RED, REVERSE], ALL], + on_alias_error: [[RED, REVERSE], ALL], + on_class_name_error:[[RED, REVERSE], ALL], + on_param_error: [[RED, REVERSE], ALL], + on___end__: [[GREEN], ALL], + } + rescue NameError + # Give up highlighting Ripper-incompatible older Ruby + TOKEN_SEQ_EXPRS = {} + end + private_constant :TOKEN_SEQ_EXPRS + + ERROR_TOKENS = TOKEN_SEQ_EXPRS.keys.select { |k| k.to_s.end_with?('error') } + private_constant :ERROR_TOKENS + + class << self + def colorable? + $stdout.tty? && (/mswin|mingw/ =~ RUBY_PLATFORM || (ENV.key?('TERM') && ENV['TERM'] != 'dumb')) + end + + def inspect_colorable?(obj, seen: {}.compare_by_identity) + case obj + when String, Symbol, Regexp, Integer, Float, FalseClass, TrueClass, NilClass + true + when Hash + without_circular_ref(obj, seen: seen) do + obj.all? { |k, v| inspect_colorable?(k, seen: seen) && inspect_colorable?(v, seen: seen) } + end + when Array + without_circular_ref(obj, seen: seen) do + obj.all? { |o| inspect_colorable?(o, seen: seen) } + end + when Range + inspect_colorable?(obj.begin, seen: seen) && inspect_colorable?(obj.end, seen: seen) + when Module + !obj.name.nil? + else + false + end + end + + def clear(colorable: colorable?) + return '' unless colorable + "\e[#{CLEAR}m" + end + + def colorize(text, seq, colorable: colorable?) + return text unless colorable + seq = seq.map { |s| "\e[#{const_get(s)}m" }.join('') + "#{seq}#{text}#{clear(colorable: colorable)}" + end + + # 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?) + return code unless colorable + + symbol_state = SymbolState.new + colored = +'' + length = 0 + end_seen = false + + 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) + end + + in_symbol = symbol_state.scan_token(token) + str.each_line do |line| + line = Reline::Unicode.escape_for_print(line) + if seq = dispatch_seq(token, expr, line, in_symbol: in_symbol) + colored << seq.map { |s| "\e[#{s}m" }.join('') + colored << line.sub(/\Z/, clear(colorable: colorable)) + else + 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) + end + + colored + end + + private + + def without_circular_ref(obj, seen:, &block) + return false if seen.key?(obj) + seen[obj] = true + block.call + ensure + seen.delete(obj) + 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 + + yield(elem.event, str, elem.state) + end + else + lexer.parse.each do |elem| + yield(elem.event, elem.tok, elem.state) + end + end + end + ensure + $VERBOSE = verbose + end + + def dispatch_seq(token, expr, str, in_symbol:) + if ERROR_TOKENS.include?(token) + TOKEN_SEQ_EXPRS[token][0] + elsif in_symbol + [YELLOW] + elsif TOKEN_KEYWORDS.fetch(token, []).include?(str) + [CYAN, BOLD] + elsif (seq, exprs = TOKEN_SEQ_EXPRS[token]; (expr & (exprs || 0)) != 0) + seq + else + nil + end + end + end + + # A class to manage a state to know whether the current token is for Symbol or not. + class SymbolState + def initialize + # Push `true` to detect Symbol. `false` to increase the nest level for non-Symbol. + @stack = [] + end + + # Return true if the token is a part of Symbol. + def scan_token(token) + prev_state = @stack.last + 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 + if @stack.last # Pop only when it's Symbol + @stack.pop + return prev_state + end + when :on_tstring_beg + @stack << false + when :on_embexpr_beg + @stack << false + return prev_state + when :on_tstring_end # :on_tstring_end may close Symbol + @stack.pop + return prev_state + when :on_embexpr_end + @stack.pop + end + @stack.last + end + end + private_constant :SymbolState + end +end diff --git a/lib/irb/color_printer.rb b/lib/irb/color_printer.rb new file mode 100644 index 0000000000..30c6825750 --- /dev/null +++ b/lib/irb/color_printer.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require 'pp' +require 'irb/color' + +module IRB + class ColorPrinter < ::PP + class << self + def pp(obj, out = $>, width = screen_width) + q = ColorPrinter.new(out, width) + q.guard_inspect_key {q.pp obj} + q.flush + out << "\n" + end + + private + + def screen_width + Reline.get_screen_size.last + rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN> + 79 + end + end + + def pp(obj) + if obj.is_a?(String) + # Avoid calling Ruby 2.4+ String#pretty_print that splits a string by "\n" + text(obj.inspect) + else + super + end + end + + def text(str, width = nil) + unless str.is_a?(String) + str = str.inspect + end + width ||= str.length + + case str + when /\A#</, '=', '>' + super(Color.colorize(str, [:GREEN]), width) + else + super(Color.colorize_code(str, ignore_error: true), width) + end + end + end +end diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index 000658e2a3..9121174a50 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -1,205 +1,443 @@ +# frozen_string_literal: false # -# irb/completor.rb - +# irb/completion.rb - # $Release Version: 0.9$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ishitsuka.com) # From Original Idea of shugo@ruby-lang.org # -require "readline" +require_relative 'ruby-lex' module IRB - module InputCompletor - - @RCS_ID='-$Id$-' - - ReservedWords = [ - "BEGIN", "END", - "alias", "and", - "begin", "break", - "case", "class", - "def", "defined", "do", - "else", "elsif", "end", "ensure", - "false", "for", - "if", "in", - "module", - "next", "nil", "not", - "or", - "redo", "rescue", "retry", "return", - "self", "super", - "then", "true", - "undef", "unless", "until", - "when", "while", - "yield", + module InputCompletor # :nodoc: + + + # Set of reserved words used by Ruby, you should not use these for + # constants or variables + ReservedWords = %w[ + __ENCODING__ __LINE__ __FILE__ + BEGIN END + alias and + begin break + case class + def defined? do + else elsif end ensure + false for + if in + module + next nil not + or + redo rescue retry return + self super + then true + undef unless until + when while + yield ] - - CompletionProc = proc { |input| - bind = IRB.conf[:MAIN_CONTEXT].workspace.binding - -# puts "input: #{input}" + BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{(" + + def self.absolute_path?(p) # TODO Remove this method after 2.6 EOL. + if File.respond_to?(:absolute_path?) + File.absolute_path?(p) + else + if File.absolute_path(p) == p + true + else + false + end + end + 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 ||= + ( + shortest = [] + rest = retrieve_gem_and_system_load_path.each_with_object([]) { |path, result| + begin + names = Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: path) + rescue Errno::ENOENT + nil + end + next if names.empty? + names.map! { |n| n.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '') }.sort! + shortest << names.shift + result.concat(names) + } + shortest.sort! | rest + ) + end + + def self.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 + + CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil| + if target =~ /\A(['"])([^'"]+)\Z/ + quote = $1 + actual_target = $2 + else + return nil # It's not String literal + end + tokens = RubyLex.ripper_lex_without_warning(preposing.gsub(/\s*\z/, '')) + tok = nil + tokens.reverse_each do |t| + unless [:on_lparen, :on_sp, :on_ignored_sp, :on_nl, :on_ignored_nl, :on_comment].include?(t.event) + tok = t + 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 + end + result + } + + CompletionProc = lambda { |target, preposing = nil, postposing = nil| + 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) } + end + } + + def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false) case input + when /^((["'`]).*\2)\.([^.]*)$/ + # String + receiver = $1 + message = $3 + + candidates = String.instance_methods.collect{|m| m.to_s} + if doc_namespace + "String.#{message}" + else + select_message(receiver, message, candidates) + end + when /^(\/[^\/]*\/)\.([^.]*)$/ - # Regexp - receiver = $1 - message = Regexp.quote($2) + # Regexp + receiver = $1 + message = $2 - candidates = Regexp.instance_methods(true) - select_message(receiver, message, candidates) + candidates = Regexp.instance_methods.collect{|m| m.to_s} + if doc_namespace + "Regexp.#{message}" + else + select_message(receiver, message, candidates) + end when /^([^\]]*\])\.([^.]*)$/ - # Array - receiver = $1 - message = Regexp.quote($2) + # Array + receiver = $1 + message = $2 - candidates = Array.instance_methods(true) - select_message(receiver, message, candidates) + candidates = Array.instance_methods.collect{|m| m.to_s} + if doc_namespace + "Array.#{message}" + else + select_message(receiver, message, candidates) + end when /^([^\}]*\})\.([^.]*)$/ - # Proc or Hash - receiver = $1 - message = Regexp.quote($2) + # Proc or Hash + 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}"] + else + select_message(receiver, message, proc_candidates | hash_candidates) + end - candidates = Proc.instance_methods(true) | Hash.instance_methods(true) - select_message(receiver, message, candidates) - when /^(:[^:.]*)$/ - # Symbol - if Symbol.respond_to?(:all_symbols) - sym = $1 - candidates = Symbol.all_symbols.collect{|s| ":" + s.id2name} - candidates.grep(/^#{sym}/) - else - [] - end - - when /^::([A-Z][^:\.\(]*)$/ - # Absolute Constant or class methods - receiver = $1 - candidates = Object.constants - candidates.grep(/^#{receiver}/).collect{|e| "::" + e} - - when /^(((::)?[A-Z][^:.\(]*)+)::?([^:.]*)$/ - # Constant or class methods - receiver = $1 - message = Regexp.quote($4) - begin - candidates = eval("#{receiver}.constants | #{receiver}.methods", bind) - rescue Exception - candidates = [] - end - candidates.grep(/^#{message}/).collect{|e| receiver + "::" + e} - - when /^(:[^:.]+)\.([^.]*)$/ - # Symbol - receiver = $1 - message = Regexp.quote($2) - - candidates = Symbol.instance_methods(true) - select_message(receiver, message, candidates) - - when /^(-?(0[dbo])?[0-9_]+(\.[0-9_]+)?([eE]-?[0-9]+)?)\.([^.]*)$/ - # Numeric - receiver = $1 - message = Regexp.quote($5) - - begin - candidates = eval(receiver, bind).methods - rescue Exception - candidates = [] - end - select_message(receiver, message, candidates) - - when /^(-?0x[0-9a-fA-F_]+)\.([^.]*)$/ - # Numeric(0xFFFF) - receiver = $1 - message = Regexp.quote($2) - - begin - candidates = eval(receiver, bind).methods - rescue Exception - candidates = [] - end - select_message(receiver, message, candidates) + # Symbol + return nil if doc_namespace + sym = $1 + candidates = Symbol.all_symbols.collect do |s| + ":" + s.id2name.encode(Encoding.default_external) + rescue EncodingError + # ignore + 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} + 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, "::") + end + + when /^(:[^:.]+)(\.|::)([^.]*)$/ + # Symbol + receiver = $1 + sep = $2 + message = $3 + + candidates = Symbol.instance_methods.collect{|m| m.to_s} + if doc_namespace + "Symbol.#{message}" + else + select_message(receiver, message, candidates, sep) + end + + when /^(?<num>-?(?:0[dbo])?[0-9_]+(?:\.[0-9_]+)?(?:(?:[eE][+-]?[0-9]+)?i?|r)?)(?<sep>\.|::)(?<mes>[^.]*)$/ + # Numeric + receiver = $~[:num] + sep = $~[:sep] + message = $~[:mes] + + begin + instance = eval(receiver, bind) + if doc_namespace + "#{instance.class.name}.#{message}" + else + candidates = instance.methods.collect{|m| m.to_s} + select_message(receiver, message, candidates, sep) + end + rescue Exception + if doc_namespace + nil + else + candidates = [] + end + end + + when /^(-?0x[0-9a-fA-F_]+)(\.|::)([^.]*)$/ + # Numeric(0xFFFF) + receiver = $1 + sep = $2 + message = $3 + + begin + instance = eval(receiver, bind) + if doc_namespace + "#{instance.class.name}.#{message}" + else + candidates = instance.methods.collect{|m| m.to_s} + select_message(receiver, message, candidates, sep) + end + rescue Exception + if doc_namespace + nil + else + candidates = [] + end + end when /^(\$[^.]*)$/ - candidates = global_variables.grep(Regexp.new(Regexp.quote($1))) - -# when /^(\$?(\.?[^.]+)+)\.([^.]*)$/ - when /^((\.?[^.]+)+)\.([^.]*)$/ - # variable - receiver = $1 - message = Regexp.quote($3) - - gv = eval("global_variables", bind) - lv = eval("local_variables", bind) - cv = eval("self.class.constants", bind) - - if (gv | lv | cv).include?(receiver) - # foo.func and foo is local var. - candidates = eval("#{receiver}.methods", bind) - elsif /^[A-Z]/ =~ receiver and /\./ !~ receiver - # Foo::Bar.func - begin - candidates = eval("#{receiver}.methods", bind) - rescue Exception - candidates = [] - end - else - # func1.func2 - candidates = [] - ObjectSpace.each_object(Module){|m| - begin - name = m.name - rescue Exception - name = "" - end - next if name != "IRB::Context" and - /^(IRB|SLex|RubyLex|RubyToken)/ =~ name - candidates.concat m.instance_methods(false) - } - candidates.sort! - candidates.uniq! - end - select_message(receiver, message, candidates) + # global var + gvar = $1 + all_gvars = global_variables.collect{|m| m.to_s} + if doc_namespace + all_gvars.find{ |i| i == gvar } + else + all_gvars.grep(Regexp.new(Regexp.quote(gvar))) + end + + when /^([^.:"].*)(\.|::)([^.]*)$/ + # variable.func or func.func + receiver = $1 + 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} + + if (gv | lv | iv | cv).include?(receiver) or /^[A-Z]/ =~ receiver && /\./ !~ receiver + # foo.func and foo is var. OR + # foo::func and foo is var. OR + # foo::Const and foo is var. OR + # Foo::Bar.func + begin + candidates = [] + rec = eval(receiver, bind) + if sep == "::" and rec.kind_of?(Module) + candidates = rec.constants.collect{|m| m.to_s} + end + candidates |= rec.methods.collect{|m| m.to_s} + rescue Exception + candidates = [] + end + 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 }}" + else + select_message(receiver, message, candidates, sep) + end when /^\.([^.]*)$/ - # unknown(maybe String) + # unknown(maybe String) - receiver = "" - message = Regexp.quote($1) + receiver = "" + message = $1 - candidates = String.instance_methods(true) - select_message(receiver, message, candidates) + 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) + end else - candidates = eval("methods | private_methods | local_variables | self.class.constants", bind) - - (candidates|ReservedWords).grep(/^#{Regexp.quote(input)}/) + if doc_namespace + vars = eval("local_variables | instance_variables", bind).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) + else + candidates = eval("methods | private_methods | local_variables | instance_variables | self.class.constants", bind).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 |= ReservedWords + candidates.grep(/^#{Regexp.quote(input)}/) + 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 } - Operators = ["%", "&", "*", "**", "+", "-", "/", - "<", "<<", "<=", "<=>", "==", "===", "=~", ">", ">=", ">>", - "[]", "[]=", "^",] - - def self.select_message(receiver, message, candidates) - candidates.grep(/^#{message}/).collect do |e| - case e - when /^[a-zA-Z_]/ - receiver + "." + e - when /^[0-9]/ - when *Operators - #receiver + " " + e - end + # Set of available operators in Ruby + Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~] + + def self.select_message(receiver, message, candidates, sep = ".") + candidates.grep(/^#{Regexp.quote(message)}/).collect do |e| + case e + when /^[a-zA-Z_]/ + receiver + sep + e + when /^[0-9]/ + when *Operators + #receiver + " " + e + end end end - end -end -if Readline.respond_to?("basic_word_break_characters=") - Readline.basic_word_break_characters= " \t\n\"\\'`><=;|&{(" + 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 + end + + %i(IRB RubyLex).each do |sym| + next unless Object.const_defined?(sym) + scanner.call(Object.const_get(sym)) + end + + s.delete(IRB::Context) if defined?(IRB::Context) + + s + end + end end -Readline.completion_append_character = nil -Readline.completion_proc = IRB::InputCompletor::CompletionProc diff --git a/lib/irb/context.rb b/lib/irb/context.rb index d01bd4aefa..0a46c1b1d4 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -1,43 +1,64 @@ +# frozen_string_literal: false # # irb/context.rb - irb context -# $Release Version: 0.9.5$ +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # -require "irb/workspace" +# +require_relative "workspace" +require_relative "inspector" +require_relative "input-method" +require_relative "output-method" module IRB + # A class that wraps the current state of the irb session, including the + # configuration of IRB.conf. class Context + # Creates a new IRB context. # - # Arguments: - # input_method: nil -- stdin or readline - # String -- File - # other -- using this as InputMethod + # The optional +input_method+ argument: # - def initialize(irb, workspace = nil, input_method = nil, output_method = nil) + # +nil+:: uses stdin or Reidline or Readline + # +String+:: uses a File + # +other+:: uses this as InputMethod + def initialize(irb, workspace = nil, input_method = nil) @irb = irb if workspace - @workspace = workspace + @workspace = workspace else - @workspace = WorkSpace.new + @workspace = WorkSpace.new end @thread = Thread.current if defined? Thread -# @irb_level = 0 # copy of default configuration @ap_name = IRB.conf[:AP_NAME] @rc = IRB.conf[:RC] @load_modules = IRB.conf[:LOAD_MODULES] - @use_readline = IRB.conf[:USE_READLINE] - @inspect_mode = IRB.conf[:INSPECT_MODE] + if IRB.conf.has_key?(:USE_SINGLELINE) + @use_singleline = IRB.conf[:USE_SINGLELINE] + elsif IRB.conf.has_key?(:USE_READLINE) # backward compatibility + @use_singleline = IRB.conf[:USE_READLINE] + else + @use_singleline = nil + end + if IRB.conf.has_key?(:USE_MULTILINE) + @use_multiline = IRB.conf[:USE_MULTILINE] + elsif IRB.conf.has_key?(:USE_REIDLINE) # backward compatibility + @use_multiline = IRB.conf[:USE_REIDLINE] + else + @use_multiline = nil + end + @use_colorize = IRB.conf[:USE_COLORIZE] + @use_autocomplete = IRB.conf[:USE_AUTOCOMPLETE] + @verbose = IRB.conf[:VERBOSE] + @io = nil - self.math_mode = IRB.conf[:MATH_MODE] if IRB.conf[:MATH_MODE] + self.inspect_mode = IRB.conf[:INSPECT_MODE] self.use_tracer = IRB.conf[:USE_TRACER] if 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] @@ -46,126 +67,322 @@ module IRB @ignore_eof = IRB.conf[:IGNORE_EOF] @back_trace_limit = IRB.conf[:BACK_TRACE_LIMIT] - + self.prompt_mode = IRB.conf[:PROMPT_MODE] - if IRB.conf[:SINGLE_IRB] or !defined?(JobManager) - @irb_name = IRB.conf[:IRB_NAME] + if IRB.conf[:SINGLE_IRB] or !defined?(IRB::JobManager) + @irb_name = IRB.conf[:IRB_NAME] else - @irb_name = "irb#"+IRB.JobManager.n_jobs.to_s + @irb_name = IRB.conf[:IRB_NAME]+"#"+IRB.JobManager.n_jobs.to_s end @irb_path = "(" + @irb_name + ")" case input_method when nil - case use_readline? - when nil - if (defined?(ReadlineInputMethod) && STDIN.tty? && - IRB.conf[:PROMPT_MODE] != :INF_RUBY) - @io = ReadlineInputMethod.new - else - @io = StdioInputMethod.new - end - when false - @io = StdioInputMethod.new - when true - if defined?(ReadlineInputMethod) - @io = ReadlineInputMethod.new - else - @io = StdioInputMethod.new - end - end + @io = nil + case use_multiline? + 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 + else + @io = nil + end + when false + @io = nil + when true + @io = ReidlineInputMethod.new + end + unless @io + case use_singleline? + when nil + if (defined?(ReadlineInputMethod) && STDIN.tty? && + IRB.conf[:PROMPT_MODE] != :INF_RUBY) + @io = ReadlineInputMethod.new + else + @io = nil + end + when false + @io = nil + when true + if defined?(ReadlineInputMethod) + @io = ReadlineInputMethod.new + else + @io = nil + end + else + @io = nil + end + end + @io = StdioInputMethod.new unless @io when String - @io = FileInputMethod.new(input_method) - @irb_name = File.basename(input_method) - @irb_path = input_method + @io = FileInputMethod.new(input_method) + @irb_name = File.basename(input_method) + @irb_path = input_method else - @io = input_method + @io = input_method end self.save_history = IRB.conf[:SAVE_HISTORY] if IRB.conf[:SAVE_HISTORY] - if output_method - @output_method = output_method - else - @output_method = StdioOutputMethod.new - end + @extra_doc_dirs = IRB.conf[:EXTRA_DOC_DIRS] - @verbose = IRB.conf[:VERBOSE] @echo = IRB.conf[:ECHO] if @echo.nil? - @echo = true + @echo = true + end + + @echo_on_assignment = IRB.conf[:ECHO_ON_ASSIGNMENT] + if @echo_on_assignment.nil? + @echo_on_assignment = :truncate + end + + @newline_before_multiline_output = IRB.conf[:NEWLINE_BEFORE_MULTILINE_OUTPUT] + if @newline_before_multiline_output.nil? + @newline_before_multiline_output = true end - @debug_level = IRB.conf[:DEBUG_LEVEL] end + # The top-level workspace, see WorkSpace#main def 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 + # context is created. See ::new for more # information on +input_method+. attr_accessor :io - + + # Current irb session. attr_accessor :irb + # A copy of the default <code>IRB.conf[:AP_NAME]</code> attr_accessor :ap_name + # A copy of the default <code>IRB.conf[:RC]</code> attr_accessor :rc + # A copy of the default <code>IRB.conf[:LOAD_MODULES]</code> attr_accessor :load_modules + # 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 - attr_reader :use_readline + # Whether multiline editor mode is enabled or not. + # + # A copy of the default <code>IRB.conf[:USE_MULTILINE]</code> + attr_reader :use_multiline + # Whether singleline editor mode is enabled or not. + # + # A copy of the default <code>IRB.conf[:USE_SINGLELINE]</code> + attr_reader :use_singleline + # Whether colorization is enabled or not. + # + # A copy of the default <code>IRB.conf[:USE_COLORIZE]</code> + attr_reader :use_colorize + # A copy of the default <code>IRB.conf[:USE_AUTOCOMPLETE]</code> + attr_reader :use_autocomplete + # A copy of the default <code>IRB.conf[:INSPECT_MODE]</code> attr_reader :inspect_mode + # A copy of the default <code>IRB.conf[:PROMPT_MODE]</code> attr_reader :prompt_mode + # Standard IRB prompt. + # + # See IRB@Customizing+the+IRB+Prompt for more information. attr_accessor :prompt_i + # IRB prompt for continuated strings. + # + # See IRB@Customizing+the+IRB+Prompt 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. attr_accessor :prompt_c + # See IRB@Customizing+the+IRB+Prompt for more information. attr_accessor :prompt_n + # Can be either the default <code>IRB.conf[:AUTO_INDENT]</code>, or the + # mode set by #prompt_mode= + # + # To disable auto-indentation in irb: + # + # IRB.conf[:AUTO_INDENT] = false + # + # or + # + # irb_context.auto_indent_mode = false + # + # or + # + # IRB.CurrentContext.auto_indent_mode = false + # + # See IRB@Configuration for more information. attr_accessor :auto_indent_mode + # The format of the return statement, set by #prompt_mode= using the + # +:RETURN+ of the +mode+ passed to set the current #prompt_mode. attr_accessor :return_format + # Whether <code>^C</code> (+control-c+) will be ignored or not. + # + # If set to +false+, <code>^C</code> will quit irb. + # + # If set to +true+, + # + # * during input: cancel input then return to top level. + # * during execute: abandon current execution. attr_accessor :ignore_sigint + # Whether <code>^D</code> (+control-d+) will be ignored or not. + # + # If set to +false+, <code>^D</code> will quit irb. attr_accessor :ignore_eof + # Specify the installation locations of the ri file to be displayed in the + # document dialog. + attr_accessor :extra_doc_dirs + # Whether to echo the return value to output or not. + # + # Uses <code>IRB.conf[:ECHO]</code> if available, or defaults to +true+. + # + # puts "hello" + # # hello + # #=> nil + # IRB.CurrentContext.echo = false + # puts "omg" + # # omg attr_accessor :echo + # Whether to echo for assignment expressions. + # + # If set to +false+, the value of assignment will not be shown. + # + # If set to +true+, the value of assignment will be shown. + # + # If set to +:truncate+, the value of assignment will be shown and truncated. + # + # It defaults to +:truncate+. + # + # a = "omg" + # #=> omg + # + # a = "omg" * 10 + # #=> omgomgomgomgomgomgomg... + # + # IRB.CurrentContext.echo_on_assignment = false + # a = "omg" + # + # IRB.CurrentContext.echo_on_assignment = true + # a = "omg" * 10 + # #=> omgomgomgomgomgomgomgomgomgomg + # + # To set the behaviour of showing on assignment in irb: + # + # IRB.conf[:ECHO_ON_ASSIGNMENT] = :truncate or true or false + # + # or + # + # irb_context.echo_on_assignment = :truncate or true or false + # + # or + # + # IRB.CurrentContext.echo_on_assignment = :truncate or true or false + attr_accessor :echo_on_assignment + # Whether a newline is put before multiline output. + # + # Uses <code>IRB.conf[:NEWLINE_BEFORE_MULTILINE_OUTPUT]</code> if available, + # or defaults to +true+. + # + # "abc\ndef" + # #=> + # abc + # def + # IRB.CurrentContext.newline_before_multiline_output = false + # "abc\ndef" + # #=> abc + # def + attr_accessor :newline_before_multiline_output + # Whether verbose messages are displayed or not. + # + # A copy of the default <code>IRB.conf[:VERBOSE]</code> attr_accessor :verbose - attr_reader :debug_level + # The limit of backtrace lines displayed as top +n+ and tail +n+. + # + # 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 - alias use_readline? use_readline + # 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 + # backward compatibility + alias use_reidline? use_multiline + # backward compatibility + alias use_readline use_singleline + # backward compatibility + alias use_readline? use_singleline + # Alias for #use_colorize + alias use_colorize? use_colorize + # Alias for #use_autocomplete + alias use_autocomplete? use_autocomplete + # Alias for #rc alias rc? rc alias ignore_sigint? ignore_sigint alias ignore_eof? ignore_eof alias echo? echo + alias echo_on_assignment? echo_on_assignment + alias newline_before_multiline_output? newline_before_multiline_output + # Returns whether messages are displayed or not. def verbose? if @verbose.nil? - if defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod) - false - elsif !STDIN.tty? or @io.kind_of?(FileInputMethod) - true - else - false - end + if @io.kind_of?(ReidlineInputMethod) + false + elsif defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod) + false + elsif !STDIN.tty? or @io.kind_of?(FileInputMethod) + true + else + false + end + else + @verbose end end + # Whether #verbose? is +true+, and +input_method+ is either + # StdioInputMethod or ReidlineInputMethod or ReadlineInputMethod, see #io + # for more information. def prompting? verbose? || (STDIN.tty? && @io.kind_of?(StdioInputMethod) || - (defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod))) + @io.kind_of?(ReidlineInputMethod) || + (defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod))) end + # The return value of the last statement evaluated. attr_reader :last_value + # Sets the return value from the last statement evaluated in this context + # to #last_value. def set_last_value(value) @last_value = value - @workspace.evaluate self, "_ = IRB.CurrentContext.last_value" + @workspace.local_variable_set :_, value end - attr_reader :irb_name - + # Sets the +mode+ of the prompt in this context. + # + # See IRB@Customizing+the+IRB+Prompt for more information. def prompt_mode=(mode) @prompt_mode = mode pconf = IRB.conf[:PROMPT][mode] @@ -174,78 +391,124 @@ module IRB @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) - @auto_indent_mode = ai + @auto_indent_mode = ai else - @auto_indent_mode = IRB.conf[:AUTO_INDENT] + @auto_indent_mode = IRB.conf[:AUTO_INDENT] end end - + + # Whether #inspect_mode is set or not, see #inspect_mode= for more detail. def inspect? @inspect_mode.nil? or @inspect_mode end + # Whether #io uses a File for the +input_method+ passed when creating the + # current context, see ::new def file_input? @io.class == FileInputMethod end + # Specifies the inspect mode with +opt+: + # + # +true+:: display +inspect+ + # +false+:: display +to_s+ + # +nil+:: inspect mode in non-math mode, + # non-inspect mode in math mode + # + # See IRB::Inspector for more information. + # + # 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 opt - @inspect_mode = opt + + if i = Inspector::INSPECTORS[opt] + @inspect_mode = opt + @inspect_method = i + i.init else - @inspect_mode = !@inspect_mode + case opt + when nil + if Inspector.keys_with_inspector(Inspector::INSPECTORS[true]).include?(@inspect_mode) + self.inspect_mode = false + elsif Inspector.keys_with_inspector(Inspector::INSPECTORS[false]).include?(@inspect_mode) + self.inspect_mode = true + else + puts "Can't switch inspect mode." + return + end + when /^\s*\{.*\}\s*$/ + begin + inspector = eval "proc#{opt}" + rescue Exception + puts "Can't switch inspect mode(#{opt})." + return + end + self.inspect_mode = inspector + when Proc + self.inspect_mode = IRB::Inspector(opt) + when Inspector + prefix = "usr%d" + i = 1 + while Inspector::INSPECTORS[format(prefix, i)]; i += 1; end + @inspect_mode = format(prefix, i) + @inspect_method = opt + Inspector.def_inspector(format(prefix, i), @inspect_method) + else + puts "Can't switch inspect mode(#{opt})." + return + end end print "Switch to#{unless @inspect_mode; ' non';end} inspect mode.\n" if verbose? @inspect_mode end - def use_readline=(opt) - @use_readline = opt - print "use readline module\n" if @use_readline - end - - def debug_level=(value) - @debug_level = value - RubyLex.debug_level = value - SLex.debug_level = value - end - - def debug? - @debug_level > 0 - end - - def evaluate(line, line_no) + def evaluate(line, line_no, exception: nil) # :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) + end set_last_value(@workspace.evaluate(self, line, irb_path, line_no)) -# @workspace.evaluate("_ = IRB.conf[:MAIN_CONTEXT]._") -# @_ = @workspace.evaluate(line, irb_path, line_no) + end + + def inspect_last_value # :nodoc: + @inspect_method.inspect_value(@last_value) end alias __exit__ exit + # Exits the current session, see IRB.irb_exit def exit(ret = 0) IRB.irb_exit(@irb, ret) + rescue UncaughtThrowError + super end - NOPRINTING_IVARS = ["@last_value"] - NO_INSPECTING_IVARS = ["@irb", "@io"] - IDNAME_IVARS = ["@prompt_mode"] + NOPRINTING_IVARS = ["@last_value"] # :nodoc: + NO_INSPECTING_IVARS = ["@irb", "@io"] # :nodoc: + IDNAME_IVARS = ["@prompt_mode"] # :nodoc: alias __inspect__ inspect - def inspect + def inspect # :nodoc: array = [] for ivar in instance_variables.sort{|e1, e2| e1 <=> e2} - name = ivar.sub(/^@(.*)$/){$1} - val = instance_eval(ivar) - case ivar - when *NOPRINTING_IVARS - array.push format("conf.%s=%s", name, "...") - when *NO_INSPECTING_IVARS - array.push format("conf.%s=%s", name, val.to_s) - when *IDNAME_IVARS - array.push format("conf.%s=:%s", name, val.id2name) - else - array.push format("conf.%s=%s", name, val.inspect) - end + ivar = ivar.to_s + name = ivar.sub(/^@(.*)$/, '\1') + val = instance_eval(ivar) + case ivar + when *NOPRINTING_IVARS + array.push format("conf.%s=%s", name, "...") + when *NO_INSPECTING_IVARS + array.push format("conf.%s=%s", name, val.to_s) + when *IDNAME_IVARS + array.push format("conf.%s=:%s", name, val.id2name) + else + array.push format("conf.%s=%s", name, val.inspect) + end end array.join("\n") end diff --git a/lib/irb/easter-egg.rb b/lib/irb/easter-egg.rb new file mode 100644 index 0000000000..3e79692de9 --- /dev/null +++ b/lib/irb/easter-egg.rb @@ -0,0 +1,138 @@ +require "reline" + +module IRB + class << self + class Vec + def initialize(x, y, z) + @x, @y, @z = x, y, z + end + + attr_reader :x, :y, :z + + def sub(other) + Vec.new(@x - other.x, @y - other.y, @z - other.z) + end + + def dot(other) + @x*other.x + @y*other.y + @z*other.z + end + + def cross(other) + ox, oy, oz = other.x, other.y, other.z + Vec.new(@y*oz-@z*oy, @z*ox-@x*oz, @x*oy-@y*ox) + end + + def normalize + r = Math.sqrt(self.dot(self)) + Vec.new(@x / r, @y / r, @z / r) + end + end + + class Canvas + def initialize((h, w)) + @data = (0..h-2).map { [0] * w } + @scale = [w / 2.0, h-2].min + @center = Complex(w / 2, h-2) + end + + def line((x1, y1), (x2, y2)) + p1 = Complex(x1, y1) / 2 * @scale + @center + p2 = Complex(x2, y2) / 2 * @scale + @center + line0(p1, p2) + end + + private def line0(p1, p2) + mid = (p1 + p2) / 2 + if (p1 - p2).abs < 1 + x, y = mid.rect + @data[y / 2][x] |= (y % 2 > 1 ? 2 : 1) + else + line0(p1, mid) + line0(p2, mid) + end + end + + def draw + @data.each {|row| row.fill(0) } + yield + @data.map {|row| row.map {|n| " ',;"[n] }.join }.join("\n") + end + end + + class RubyModel + def initialize + @faces = init_ruby_model + end + + def init_ruby_model + cap_vertices = (0..5).map {|i| Vec.new(*Complex.polar(1, i * Math::PI / 3).rect, 1) } + middle_vertices = (0..5).map {|i| Vec.new(*Complex.polar(2, (i + 0.5) * Math::PI / 3).rect, 0) } + bottom_vertex = Vec.new(0, 0, -2) + + faces = [cap_vertices] + 6.times do |j| + i = j-1 + faces << [cap_vertices[i], middle_vertices[i], cap_vertices[j]] + faces << [cap_vertices[j], middle_vertices[i], middle_vertices[j]] + faces << [middle_vertices[i], bottom_vertex, middle_vertices[j]] + end + + faces + end + + def render_frame(i) + angle = i / 10.0 + dir = Vec.new(*Complex.polar(1, angle).rect, Math.sin(angle)).normalize + dir2 = Vec.new(*Complex.polar(1, angle - Math::PI/2).rect, 0) + up = dir.cross(dir2) + nm = dir.cross(up) + @faces.each do |vertices| + v0, v1, v2, = vertices + if v1.sub(v0).cross(v2.sub(v0)).dot(dir) > 0 + points = vertices.map {|p| [nm.dot(p), up.dot(p)] } + (points + [points[0]]).each_cons(2) do |p1, p2| + yield p1, p2 + end + end + end + end + 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 + end + when :dancing + begin + canvas = Canvas.new(Reline.get_screen_size) + Reline::IOGate.set_winch_handler do + canvas = Canvas.new(Reline.get_screen_size) + end + ruby_model = RubyModel.new + print "\e[?1049h" + 0.step do |i| # TODO (0..).each needs Ruby 2.6 or later + buff = canvas.draw do + ruby_model.render_frame(i) do |p1, p2| + canvas.line(p1, p2) + end + end + buff[0, 20] = "\e[0mPress Ctrl+C to stop\e[31m\e[1m" + print "\e[H" + buff + sleep 0.05 + end + rescue Interrupt + ensure + print "\e[0m\e[?1049l" + end + end + end + end +end + +IRB.__send__(:easter_egg, ARGV[0]&.to_sym) if $0 == __FILE__ diff --git a/lib/irb/ext/change-ws.rb b/lib/irb/ext/change-ws.rb index fff8f58fe5..4c57e44eab 100644 --- a/lib/irb/ext/change-ws.rb +++ b/lib/irb/ext/change-ws.rb @@ -1,62 +1,45 @@ +# frozen_string_literal: false # -# irb/ext/cb.rb - -# $Release Version: 0.9.5$ +# irb/ext/cb.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -module IRB +module IRB # :nodoc: class Context + # Inherited from +TOPLEVEL_BINDING+. def home_workspace if defined? @home_workspace - @home_workspace + @home_workspace else - @home_workspace = @workspace + @home_workspace = @workspace end end + # Changes the current workspace to given object or binding. + # + # If the optional argument is omitted, the workspace will be + # #home_workspace which is inherited from +TOPLEVEL_BINDING+ or the main + # object, <code>IRB.conf[:MAIN_CONTEXT]</code> when irb was initialized. + # + # See IRB::WorkSpace.new for more information. def change_workspace(*_main) if _main.empty? - @workspace = home_workspace - return main + @workspace = home_workspace + return main end - + @workspace = WorkSpace.new(_main[0]) - + if !(class<<main;ancestors;end).include?(ExtendCommandBundle) - main.extend ExtendCommandBundle + main.extend ExtendCommandBundle end end - -# def change_binding(*_main) -# back = @workspace -# @workspace = WorkSpace.new(*_main) -# unless _main.empty? -# begin -# main.extend ExtendCommandBundle -# rescue -# print "can't change binding to: ", main.inspect, "\n" -# @workspace = back -# return nil -# end -# end -# @irb_level += 1 -# begin -# catch(:SU_EXIT) do -# @irb.eval_input -# end -# ensure -# @irb_level -= 1 -# @workspace = back -# end -# end -# alias change_workspace change_binding - end + end end - diff --git a/lib/irb/ext/history.rb b/lib/irb/ext/history.rb index 40f8692e8b..fc304c6f6c 100644 --- a/lib/irb/ext/history.rb +++ b/lib/irb/ext/history.rb @@ -1,110 +1,155 @@ +# frozen_string_literal: false # -# history.rb - -# $Release Version: 0.9.5$ +# history.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -module IRB +module IRB # :nodoc: class Context NOPRINTING_IVARS.push "@eval_history_values" + # See #set_last_value alias _set_last_value set_last_value def set_last_value(value) _set_last_value(value) -# @workspace.evaluate self, "_ = IRB.CurrentContext.last_value" - if @eval_history #and !@eval_history_values.equal?(llv) - @eval_history_values.push @line_no, @last_value - @workspace.evaluate self, "__ = IRB.CurrentContext.instance_eval{@eval_history_values}" + if defined?(@eval_history) && @eval_history + @eval_history_values.push @line_no, @last_value + @workspace.evaluate self, "__ = IRB.CurrentContext.instance_eval{@eval_history_values}" end @last_value end + remove_method :eval_history= if method_defined?(:eval_history=) + # The command result history limit. This method is not available until + # #eval_history= was called with non-nil value (directly or via + # setting <code>IRB.conf[:EVAL_HISTORY]</code> in <code>.irbrc</code>). attr_reader :eval_history + # Sets command result history limit. Default value is set from + # <code>IRB.conf[:EVAL_HISTORY]</code>. + # + # +no+ is an Integer or +nil+. + # + # Returns +no+ of history items if greater than 0. + # + # If +no+ is 0, the number of history items is unlimited. + # + # If +no+ is +nil+, execution result history isn't used (default). + # + # History values are available via <code>__</code> variable, see + # IRB::History. def eval_history=(no) if no - if defined?(@eval_history) && @eval_history - @eval_history_values.size(no) - else - @eval_history_values = History.new(no) - IRB.conf[:__TMP__EHV__] = @eval_history_values - @workspace.evaluate(self, "__ = IRB.conf[:__TMP__EHV__]") - IRB.conf.delete(:__TMP_EHV__) - end + if defined?(@eval_history) && @eval_history + @eval_history_values.size(no) + else + @eval_history_values = History.new(no) + IRB.conf[:__TMP__EHV__] = @eval_history_values + @workspace.evaluate(self, "__ = IRB.conf[:__TMP__EHV__]") + IRB.conf.delete(:__TMP_EHV__) + end else - @eval_history_values = nil + @eval_history_values = nil end @eval_history = no end end + # Represents history of results of previously evaluated commands. + # + # Available via <code>__</code> variable, only if <code>IRB.conf[:EVAL_HISTORY]</code> + # or <code>IRB::CurrentContext().eval_history</code> is non-nil integer value + # (by default it is +nil+). + # + # Example (in `irb`): + # + # # Initialize history + # IRB::CurrentContext().eval_history = 10 + # # => 10 + # + # # Perform some commands... + # 1 + 2 + # # => 3 + # puts 'x' + # # x + # # => nil + # raise RuntimeError + # # ...error raised + # + # # Inspect history (format is "<item number> <evaluated value>": + # __ + # # => 1 10 + # # 2 3 + # # 3 nil + # + # __[1] + # # => 10 + # class History - @RCS_ID='-$Id$-' - def initialize(size = 16) + def initialize(size = 16) # :nodoc: @size = size @contents = [] end - def size(size) - if size != 0 && size < @size - @contents = @contents[@size - size .. @size] + def size(size) # :nodoc: + if size != 0 && size < @size + @contents = @contents[@size - size .. @size] end @size = size end + # Get one item of the content (both positive and negative indexes work). def [](idx) begin - if idx >= 0 - @contents.find{|no, val| no == idx}[1] - else - @contents[idx][1] - end + if idx >= 0 + @contents.find{|no, val| no == idx}[1] + else + @contents[idx][1] + end rescue NameError - nil + nil end end - def push(no, val) + def push(no, val) # :nodoc: @contents.push [no, val] @contents.shift if @size != 0 && @contents.size > @size end - + alias real_inspect inspect - def inspect + def inspect # :nodoc: if @contents.empty? - return real_inspect + return real_inspect end unless (last = @contents.pop)[1].equal?(self) - @contents.push last - last = nil + @contents.push last + last = nil end str = @contents.collect{|no, val| - if val.equal?(self) - "#{no} ...self-history..." - else - "#{no} #{val.inspect}" - end + if val.equal?(self) + "#{no} ...self-history..." + else + "#{no} #{val.inspect}" + end }.join("\n") if str == "" - str = "Empty." + str = "Empty." end @contents.push last if last str end end end - - diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb index 837e2553ac..af028996e7 100644 --- a/lib/irb/ext/loader.rb +++ b/lib/irb/ext/loader.rb @@ -1,25 +1,29 @@ +# frozen_string_literal: false # -# loader.rb - -# $Release Version: 0.9.5$ +# loader.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -module IRB +module IRB # :nodoc: + # Raised in the event of an exception in a file loaded from an Irb session class LoadAbort < Exception;end + # Provides a few commands for loading files within an irb session. + # + # See ExtendCommandBundle for more information. module IrbLoader - @RCS_ID='-$Id$-' alias ruby_load load alias ruby_require require + # Loads the given file similarly to Kernel#load def irb_load(fn, priv = nil) path = search_file_from_ruby_path(fn) raise LoadError, "No such file to load -- #{fn}" unless path @@ -27,94 +31,125 @@ module IRB load_file(path, priv) end - def search_file_from_ruby_path(fn) - if /^#{Regexp.quote(File::Separator)}/ =~ fn - return fn if File.exist?(fn) - return nil + 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) + return fn if File.exist?(fn) + return nil end for path in $: - if File.exist?(f = File.join(path, fn)) - return f - end + if File.exist?(f = File.join(path, fn)) + return f + end end return nil end + # Loads a given file in the current session and displays the source lines + # + # See Irb#suspend_input_method for more information. def source_file(path) irb.suspend_name(path, File.basename(path)) do - irb.suspend_input_method(FileInputMethod.new(path)) do - |back_io| - irb.signal_status(:IN_LOAD) do - if back_io.kind_of?(FileInputMethod) - irb.eval_input - else - begin - irb.eval_input - rescue LoadAbort - print "load abort!!\n" - end - end - end - end + FileInputMethod.open(path) do |io| + irb.suspend_input_method(io) do + |back_io| + irb.signal_status(:IN_LOAD) do + if back_io.kind_of?(FileInputMethod) + irb.eval_input + else + begin + irb.eval_input + rescue LoadAbort + print "load abort!!\n" + end + end + end + end + end end end + # Loads the given file in the current session's context and evaluates it. + # + # See Irb#suspend_input_method for more information. def load_file(path, priv = nil) irb.suspend_name(path, File.basename(path)) do - - if priv - ws = WorkSpace.new(Module.new) - else - ws = WorkSpace.new - end - irb.suspend_workspace(ws) do - irb.suspend_input_method(FileInputMethod.new(path)) do - |back_io| - irb.signal_status(:IN_LOAD) do -# p irb.conf - if back_io.kind_of?(FileInputMethod) - irb.eval_input - else - begin - irb.eval_input - rescue LoadAbort - print "load abort!!\n" - end - end - end - end - end + + if priv + ws = WorkSpace.new(Module.new) + else + ws = WorkSpace.new + end + irb.suspend_workspace(ws) do + FileInputMethod.open(path) do |io| + irb.suspend_input_method(io) do + |back_io| + irb.signal_status(:IN_LOAD) do + if back_io.kind_of?(FileInputMethod) + irb.eval_input + else + begin + irb.eval_input + rescue LoadAbort + print "load abort!!\n" + end + end + end + end + end + end end end - def old + def old # :nodoc: back_io = @io back_path = @irb_path back_name = @irb_name back_scanner = @irb.scanner begin - @io = FileInputMethod.new(path) - @irb_name = File.basename(path) - @irb_path = path - @irb.signal_status(:IN_LOAD) do - if back_io.kind_of?(FileInputMethod) - @irb.eval_input - else - begin - @irb.eval_input - rescue LoadAbort - print "load abort!!\n" - end - end - end + @io = FileInputMethod.new(path) + @irb_name = File.basename(path) + @irb_path = path + @irb.signal_status(:IN_LOAD) do + if back_io.kind_of?(FileInputMethod) + @irb.eval_input + else + begin + @irb.eval_input + rescue LoadAbort + print "load abort!!\n" + end + end + end ensure - @io = back_io - @irb_name = back_name - @irb_path = back_path - @irb.scanner = back_scanner + @io = back_io + @irb_name = back_name + @irb_path = back_path + @irb.scanner = back_scanner end end end end - diff --git a/lib/irb/ext/math-mode.rb b/lib/irb/ext/math-mode.rb deleted file mode 100644 index bd443b96ed..0000000000 --- a/lib/irb/ext/math-mode.rb +++ /dev/null @@ -1,37 +0,0 @@ -# -# math-mode.rb - -# $Release Version: 0.9.5$ -# $Revision$ -# $Date$ -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# -# -- -# -# -# -require "mathn" - -module IRB - class Context - attr_reader :math_mode - alias math? math_mode - - def math_mode=(opt) - if @math_mode == true && opt == false - IRB.fail CantReturnToNormalMode - return - end - - @math_mode = opt - if math_mode - main.extend Math - print "start math mode\n" if verbose? - end - end - - def inspect? - @inspect_mode.nil? && !@math_mode or @inspect_mode - end - end -end - diff --git a/lib/irb/ext/multi-irb.rb b/lib/irb/ext/multi-irb.rb index 4589b1d554..74de1ecde5 100644 --- a/lib/irb/ext/multi-irb.rb +++ b/lib/irb/ext/multi-irb.rb @@ -1,134 +1,170 @@ +# frozen_string_literal: false # # irb/multi-irb.rb - multiple irb module -# $Release Version: 0.9.5$ +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # -IRB.fail CantShiftToMultiIrbMode unless defined?(Thread) -require "thread" +# +fail CantShiftToMultiIrbMode unless defined?(Thread) module IRB - # job management class class JobManager - @RCS_ID='-$Id$-' + # Creates a new JobManager object def initialize - # @jobs = [[thread, irb],...] @jobs = [] @current_job = nil end + # The active irb session attr_accessor :current_job + # The total number of irb sessions, used to set +irb_name+ of the current + # Context. def n_jobs @jobs.size end + # Returns the thread for the given +key+ object, see #search for more + # information. def thread(key) - th, irb = search(key) + th, = search(key) th end + # Returns the irb session for the given +key+ object, see #search for more + # information. def irb(key) - th, irb = search(key) + _, irb = search(key) irb end + # Returns the top level thread. def main_thread @jobs[0][0] end + # Returns the top level irb session. def main_irb @jobs[0][1] end + # Add the given +irb+ session to the jobs Array. def insert(irb) @jobs.push [Thread.current, irb] end + # Changes the current active irb session to the given +key+ in the jobs + # Array. + # + # Raises an IrbAlreadyDead exception if the given +key+ is no longer alive. + # + # If the given irb session is already active, an IrbSwitchedToCurrentThread + # exception is raised. def switch(key) th, irb = search(key) - IRB.fail IrbAlreadyDead unless th.alive? - IRB.fail IrbSwitchedToCurrentThread if th == Thread.current + fail IrbAlreadyDead unless th.alive? + fail IrbSwitchedToCurrentThread if th == Thread.current @current_job = irb th.run Thread.stop @current_job = irb(Thread.current) end + # Terminates the irb sessions specified by the given +keys+. + # + # Raises an IrbAlreadyDead exception if one of the given +keys+ is already + # terminated. + # + # See Thread#exit for more information. def kill(*keys) for key in keys - th, irb = search(key) - IRB.fail IrbAlreadyDead unless th.alive? - th.exit + th, _ = search(key) + fail IrbAlreadyDead unless th.alive? + th.exit end - end + end + # Returns the associated job for the given +key+. + # + # If given an Integer, it will return the +key+ index for the jobs Array. + # + # When an instance of Irb is given, it will return the irb session + # associated with +key+. + # + # If given an instance of Thread, it will return the associated thread + # +key+ using Object#=== on the jobs Array. + # + # Otherwise returns the irb session with the same top-level binding as the + # given +key+. + # + # Raises a NoSuchJob exception if no job can be found with the given +key+. def search(key) - case key - when Integer - @jobs[key] - when Irb - @jobs.find{|k, v| v.equal?(key)} - when Thread - @jobs.assoc(key) - else - assoc = @jobs.find{|k, v| v.context.main.equal?(key)} - IRB.fail NoSuchJob, key if assoc.nil? - assoc - end + job = case key + when Integer + @jobs[key] + when Irb + @jobs.find{|k, v| v.equal?(key)} + when Thread + @jobs.assoc(key) + else + @jobs.find{|k, v| v.context.main.equal?(key)} + end + fail NoSuchJob, key if job.nil? + job end + # Deletes the job at the given +key+. def delete(key) case key when Integer - IRB.fail NoSuchJob, key unless @jobs[key] - @jobs[key] = nil + fail NoSuchJob, key unless @jobs[key] + @jobs[key] = nil else - catch(:EXISTS) do - @jobs.each_index do - |i| - if @jobs[i] and (@jobs[i][0] == key || - @jobs[i][1] == key || - @jobs[i][1].context.main.equal?(key)) - @jobs[i] = nil - throw :EXISTS - end - end - IRB.fail NoSuchJob, key - end + catch(:EXISTS) do + @jobs.each_index do + |i| + if @jobs[i] and (@jobs[i][0] == key || + @jobs[i][1] == key || + @jobs[i][1].context.main.equal?(key)) + @jobs[i] = nil + throw :EXISTS + end + end + fail NoSuchJob, key + end end until assoc = @jobs.pop; end unless @jobs.empty? @jobs.push assoc end + # Outputs a list of jobs, see the irb command +irb_jobs+, or +jobs+. def inspect ary = [] @jobs.each_index do - |i| - th, irb = @jobs[i] - next if th.nil? - - if th.alive? - if th.stop? - t_status = "stop" - else - t_status = "running" - end - else - t_status = "exited" - end - ary.push format("#%d->%s on %s (%s: %s)", - i, - irb.context.irb_name, - irb.context.main, - th, - t_status) + |i| + th, irb = @jobs[i] + next if th.nil? + + if th.alive? + if th.stop? + t_status = "stop" + else + t_status = "running" + end + else + t_status = "exited" + end + ary.push format("#%d->%s on %s (%s: %s)", + i, + irb.context.irb_name, + irb.context.main, + th, + t_status) end ary.join("\n") end @@ -136,76 +172,64 @@ module IRB @JobManager = JobManager.new + # The current JobManager in the session def IRB.JobManager @JobManager end + # The current Context in this session def IRB.CurrentContext IRB.JobManager.irb(Thread.current).context end - # invoke multi-irb + # Creates a new IRB session, see Irb.new. + # + # 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) workspace = WorkSpace.new(*main) parent_thread = Thread.current Thread.start do begin - irb = Irb.new(workspace, file) - rescue - print "Subirb can't start with context(self): ", workspace.main.inspect, "\n" - print "return to main irb\n" - Thread.pass - Thread.main.wakeup - Thread.exit + irb = Irb.new(workspace, file) + rescue + print "Subirb can't start with context(self): ", workspace.main.inspect, "\n" + print "return to main irb\n" + Thread.pass + Thread.main.wakeup + Thread.exit end @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC] @JobManager.insert(irb) @JobManager.current_job = irb begin - system_exit = false - catch(:IRB_EXIT) do - irb.eval_input - end + system_exit = false + catch(:IRB_EXIT) do + irb.eval_input + end rescue SystemExit - system_exit = true - raise - #fail + system_exit = true + raise + #fail ensure - unless system_exit - @JobManager.delete(irb) - if parent_thread.alive? - @JobManager.current_job = @JobManager.irb(parent_thread) - parent_thread.run - else - @JobManager.current_job = @JobManager.main_irb - @JobManager.main_thread.run - end - end + unless system_exit + @JobManager.delete(irb) + if @JobManager.current_job == irb + if parent_thread.alive? + @JobManager.current_job = @JobManager.irb(parent_thread) + parent_thread.run + else + @JobManager.current_job = @JobManager.main_irb + @JobManager.main_thread.run + end + end + end end end Thread.stop @JobManager.current_job = @JobManager.irb(Thread.current) end -# class Context -# def set_last_value(value) -# @last_value = value -# @workspace.evaluate "_ = IRB.JobManager.irb(Thread.current).context.last_value" -# if @eval_history #and !@__.equal?(@last_value) -# @eval_history_values.push @line_no, @last_value -# @workspace.evaluate "__ = IRB.JobManager.irb(Thread.current).context.instance_eval{@eval_history_values}" -# end -# @last_value -# end -# end - -# module ExtendCommand -# def irb_context -# IRB.JobManager.irb(Thread.current).context -# end -# # alias conf irb_context -# end - @CONF[:SINGLE_IRB_MODE] = false @JobManager.insert(@CONF[:MAIN_CONTEXT].irb) @JobManager.current_job = @CONF[:MAIN_CONTEXT].irb @@ -213,22 +237,22 @@ module IRB class Irb def signal_handle unless @context.ignore_sigint? - print "\nabort!!\n" if @context.verbose? - exit + print "\nabort!!\n" if @context.verbose? + exit end case @signal_status when :IN_INPUT - print "^C\n" - IRB.JobManager.thread(self).raise RubyLex::TerminateLineInput + print "^C\n" + IRB.JobManager.thread(self).raise RubyLex::TerminateLineInput when :IN_EVAL - IRB.irb_abort(self) + IRB.irb_abort(self) when :IN_LOAD - IRB.irb_abort(self, LoadAbort) + IRB.irb_abort(self, LoadAbort) when :IN_IRB - # ignore + # ignore else - # ignore other cases as well + # ignore other cases as well end end end diff --git a/lib/irb/ext/save-history.rb b/lib/irb/ext/save-history.rb index 5260bfcdd8..7acaebe36a 100644 --- a/lib/irb/ext/save-history.rb +++ b/lib/irb/ext/save-history.rb @@ -1,85 +1,130 @@ -#!/usr/local/bin/ruby -# -# save-history.rb - -# $Release Version: 0.9.5$ +# frozen_string_literal: false +# save-history.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ -# by Keiju ISHITSUKAkeiju@ruby-lang.org) +# by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # - -require "readline" +# module IRB - module HistorySavingAbility - @RCS_ID='-$Id$-' + module HistorySavingAbility # :nodoc: end class Context - def init_save_history + def init_save_history# :nodoc: unless (class<<@io;self;end).include?(HistorySavingAbility) - @io.extend(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 + 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 - include Readline - - def HistorySavingAbility.create_finalizer - proc do - if num = IRB.conf[:SAVE_HISTORY] and (num = num.to_i) > 0 - if hf = IRB.conf[:HISTORY_FILE] - file = File.expand_path(hf) - end - file = IRB.rc_file("_history") unless file - open(file, 'w' ) do |f| - hist = HISTORY.to_a - f.puts(hist[-num..-1] || hist) - end - end - end - end - + module HistorySavingAbility # :nodoc: def HistorySavingAbility.extended(obj) - ObjectSpace.define_finalizer(obj, HistorySavingAbility.create_finalizer) + IRB.conf[:AT_EXIT].push proc{obj.save_history} obj.load_history obj end def load_history - hist = IRB.conf[:HISTORY_FILE] - hist = IRB.rc_file("_history") unless hist - if File.exist?(hist) - open(hist) do |f| - f.each {|l| HISTORY << l.chomp} - end + 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 805f630a4d..67ac4bb965 100644 --- a/lib/irb/ext/tracer.rb +++ b/lib/irb/ext/tracer.rb @@ -1,15 +1,28 @@ +# frozen_string_literal: false # -# irb/lib/tracer.rb - -# $Release Version: 0.9.5$ +# irb/lib/tracer.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # -require "tracer" +# +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 + return # This is about to disable loading below +end module IRB @@ -19,22 +32,29 @@ module IRB Tracer.add_filter { |event, file, line, id, binding, *rests| /^#{Regexp.quote(@CONF[:IRB_LIB_PATH])}/ !~ file and - File::basename(file) != "irb.rb" + 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) - } + Tracer.set_get_line_procs(@irb_path) { + |line_no, *rests| + @io.line(line_no) + } elsif !opt && @use_tracer - Tracer.off + Tracer.off end @use_tracer=opt end @@ -42,20 +62,23 @@ module IRB 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 - end + Tracer.on + begin + __evaluate__(context, statements, file, line) + ensure + Tracer.off + end else - __evaluate__(context, statements, file || __FILE__, line || __LINE__) + __evaluate__(context, statements, file || __FILE__, line || __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 1b4d480fcd..1897bc89e0 100644 --- a/lib/irb/ext/use-loader.rb +++ b/lib/irb/ext/use-loader.rb @@ -1,17 +1,17 @@ +# frozen_string_literal: false # -# use-loader.rb - -# $Release Version: 0.9.5$ +# use-loader.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -require "irb/cmd/load" -require "irb/ext/loader" +require_relative "../cmd/load" +require_relative "loader" class Object alias __original__load__IRB_use_loader__ load @@ -20,9 +20,13 @@ end module IRB module ExtendCommandBundle + 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) 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) end @@ -31,35 +35,41 @@ module IRB class Context IRB.conf[:USE_LOADER] = false - + + # Returns whether +irb+'s own file reader method is used by + # +load+/+require+ or not. + # + # This mode is globally affected (irb-wide). def use_loader IRB.conf[:USE_LOADER] end alias use_loader? use_loader + remove_method :use_loader= if method_defined?(:use_loader=) + # Sets <code>IRB.conf[:USE_LOADER]</code> + # + # See #use_loader for more information. def use_loader=(opt) 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 { - alias_method :load, :irb_load - alias_method :require, :irb_require - } - else - (class<<@workspace.main;self;end).instance_eval { - alias_method :load, :__original__load__IRB_use_loader__ - alias_method :require, :__original__require__IRB_use_loader__ - } - end + IRB.conf[:USE_LOADER] = opt + if opt + if !$".include?("irb/cmd/load") + end + (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 { + alias_method :load, :__original__load__IRB_use_loader__ + alias_method :require, :__original__require__IRB_use_loader__ + } + end end print "Switch to load/require#{unless use_loader; ' non';end} trace mode.\n" if verbose? opt end end end - - diff --git a/lib/irb/ext/workspaces.rb b/lib/irb/ext/workspaces.rb index 79098570dc..730b58e64d 100644 --- a/lib/irb/ext/workspaces.rb +++ b/lib/irb/ext/workspaces.rb @@ -1,56 +1,66 @@ +# frozen_string_literal: false # -# push-ws.rb - -# $Release Version: 0.9.5$ +# push-ws.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -module IRB +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 + @workspaces else - @workspaces = [] + @workspaces = [] end end + # Creates a new workspace with the given object or binding, and appends it + # onto the current #workspaces stack. + # + # See IRB::Context#change_workspace and IRB::WorkSpace.new for more + # information. def push_workspace(*_main) if _main.empty? - if workspaces.empty? - print "No other workspace\n" - return nil - end - ws = workspaces.pop - workspaces.push @workspace - @workspace = ws - return workspaces + if workspaces.empty? + print "No other workspace\n" + return nil + 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 + main.extend ExtendCommandBundle end end + # Removes the last element from the current #workspaces stack and returns + # it, or +nil+ if the current workspace stack is empty. + # + # Also, see #push_workspace. def pop_workspace if workspaces.empty? - print "workspace stack empty\n" - return + print "workspace stack empty\n" + return end @workspace = workspaces.pop end end end - diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb index 8994f2f8d2..339e9e6084 100644 --- a/lib/irb/extend-command.rb +++ b/lib/irb/extend-command.rb @@ -1,29 +1,38 @@ +# frozen_string_literal: false # -# irb/extend-command.rb - irb extend command -# $Release Version: 0.9.5$ +# irb/extend-command.rb - irb extend command +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # -module IRB - # - # IRB extended command - # +# +module IRB # :nodoc: + # Installs the default irb extensions command bundle. module ExtendCommandBundle - EXCB = 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 @@ -37,188 +46,264 @@ module IRB ] @EXTEND_COMMANDS = [ - [:irb_current_working_workspace, :CurrentWorkingWorkspace, "irb/cmd/chws", - [:irb_print_working_workspace, OVERRIDE_ALL], - [:irb_cwws, OVERRIDE_ALL], - [:irb_pwws, OVERRIDE_ALL], -# [:irb_cww, OVERRIDE_ALL], -# [:irb_pww, OVERRIDE_ALL], - [:cwws, NO_OVERRIDE], - [:pwws, NO_OVERRIDE], -# [:cww, NO_OVERRIDE], -# [:pww, NO_OVERRIDE], - [:irb_current_working_binding, OVERRIDE_ALL], - [:irb_print_working_binding, OVERRIDE_ALL], - [:irb_cwb, OVERRIDE_ALL], - [:irb_pwb, OVERRIDE_ALL], -# [:cwb, NO_OVERRIDE], -# [:pwb, NO_OVERRIDE] + [ + :irb_current_working_workspace, :CurrentWorkingWorkspace, "irb/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, "irb/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, "irb/cmd/pushws", + [:workspaces, NO_OVERRIDE], + [:irb_bindings, OVERRIDE_ALL], + [:bindings, NO_OVERRIDE], + ], + [ + :irb_push_workspace, :PushWorkspace, "irb/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, "irb/cmd/pushws", + [:irb_popws, OVERRIDE_ALL], + [:popws, NO_OVERRIDE], + [:irb_pop_binding, OVERRIDE_ALL], + [:irb_popb, OVERRIDE_ALL], + [:popb, NO_OVERRIDE], + ], + + [ + :irb_load, :Load, "irb/cmd/load"], + [ + :irb_require, :Require, "irb/cmd/load"], + [ + :irb_source, :Source, "irb/cmd/load", + [:source, NO_OVERRIDE], + ], + + [ + :irb, :IrbCommand, "irb/cmd/subirb"], + [ + :irb_jobs, :Jobs, "irb/cmd/subirb", + [:jobs, NO_OVERRIDE], + ], + [ + :irb_fg, :Foreground, "irb/cmd/subirb", + [:fg, NO_OVERRIDE], + ], + [ + :irb_kill, :Kill, "irb/cmd/subirb", + [:kill, OVERRIDE_PRIVATE_ONLY], + ], + + [ + :irb_help, :Help, "irb/cmd/help", + [:help, NO_OVERRIDE], + ], + + [ + :irb_info, :Info, "irb/cmd/info" + ], + + [ + :irb_ls, :Ls, "irb/cmd/ls", + [:ls, NO_OVERRIDE], + ], + + [ + :irb_measure, :Measure, "irb/cmd/measure", + [:measure, NO_OVERRIDE], + ], + + [ + :irb_show_source, :ShowSource, "irb/cmd/show_source", + [:show_source, NO_OVERRIDE], + ], + + [ + :irb_whereami, :Whereami, "irb/cmd/whereami", + [:whereami, NO_OVERRIDE], ], - [:irb_change_workspace, :ChangeWorkspace, "irb/cmd/chws", - [:irb_chws, OVERRIDE_ALL], -# [:irb_chw, OVERRIDE_ALL], - [:irb_cws, OVERRIDE_ALL], -# [:irb_cw, OVERRIDE_ALL], - [:chws, NO_OVERRIDE], -# [:chw, NO_OVERRIDE], - [:cws, NO_OVERRIDE], -# [:cw, NO_OVERRIDE], - [:irb_change_binding, OVERRIDE_ALL], - [:irb_cb, OVERRIDE_ALL], - [:cb, NO_OVERRIDE]], - - [:irb_workspaces, :Workspaces, "irb/cmd/pushws", - [:workspaces, NO_OVERRIDE], - [:irb_bindings, OVERRIDE_ALL], - [:bindings, NO_OVERRIDE]], - [:irb_push_workspace, :PushWorkspace, "irb/cmd/pushws", - [:irb_pushws, OVERRIDE_ALL], -# [:irb_pushw, OVERRIDE_ALL], - [:pushws, NO_OVERRIDE], -# [:pushw, NO_OVERRIDE], - [:irb_push_binding, OVERRIDE_ALL], - [:irb_pushb, OVERRIDE_ALL], - [:pushb, NO_OVERRIDE]], - [:irb_pop_workspace, :PopWorkspace, "irb/cmd/pushws", - [:irb_popws, OVERRIDE_ALL], -# [:irb_popw, OVERRIDE_ALL], - [:popws, NO_OVERRIDE], -# [:popw, NO_OVERRIDE], - [:irb_pop_binding, OVERRIDE_ALL], - [:irb_popb, OVERRIDE_ALL], - [:popb, NO_OVERRIDE]], - - [:irb_load, :Load, "irb/cmd/load"], - [:irb_require, :Require, "irb/cmd/load"], - [:irb_source, :Source, "irb/cmd/load", - [:source, NO_OVERRIDE]], - - [:irb, :IrbCommand, "irb/cmd/subirb"], - [:irb_jobs, :Jobs, "irb/cmd/subirb", - [:jobs, NO_OVERRIDE]], - [:irb_fg, :Foreground, "irb/cmd/subirb", - [:fg, NO_OVERRIDE]], - [:irb_kill, :Kill, "irb/cmd/subirb", - [:kill, OVERRIDE_PRIVATE_ONLY]], - - [:irb_help, :Help, "irb/cmd/help", - [:help, 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) + def_extend_command(*args) end end - # aliases = [commans_alias, flag], ... + # 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 + cmd_class = cmd_class.id2name when String when Class - cmd_class = cmd_class.name + cmd_class = cmd_class.name end if load_file - eval %[ - def #{cmd_name}(*opts, &b) - require "#{load_file}" - eval %[ - def #{cmd_name}(*opts, &b) - ExtendCommand::#{cmd_class}.execute(irb_context, *opts, &b) - end - ] - send :#{cmd_name}, *opts, &b - end - ] + kwargs = ", **kwargs" if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.7.0" + line = __LINE__; eval %[ + def #{cmd_name}(*opts#{kwargs}, &b) + require "#{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 - eval %[ - def #{cmd_name}(*opts, &b) - ExtendCommand::#{cmd_class}.execute(irb_context, *opts, &b) - end - ] + 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] + @ALIASES.push [ali, cmd_name, flag] end end - # override = {NO_OVERRIDE, OVERRIDE_PRIVATE_ONLY, OVERRIDE_ALL} + # 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 - } + (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 - print "irb: warn: can't alias #{to} from #{from}.\n" + print "irb: warn: can't alias #{to} from #{from}.\n" end end - def self.irb_original_method_name(method_name) + 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 + 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 - # extension support for Context + # Extends methods for the Context module module ContextExtender - CE = ContextExtender + CE = ContextExtender # :nodoc: @EXTEND_COMMANDS = [ [:eval_history=, "irb/ext/history.rb"], [:use_tracer=, "irb/ext/tracer.rb"], - [:math_mode=, "irb/ext/math-mode.rb"], [:use_loader=, "irb/ext/use-loader.rb"], [:save_history=, "irb/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) + 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) - Context.module_eval %[ + line = __LINE__; Context.module_eval %[ def #{cmd_name}(*opts, &b) - Context.module_eval {remove_method(:#{cmd_name})} - require "#{load_file}" - send :#{cmd_name}, *opts, &b - end - for ali in aliases - alias_method ali, cmd_name - end - ] + Context.module_eval {remove_method(:#{cmd_name})} + require "#{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 @@ -227,12 +312,14 @@ module IRB module_eval %[ alias_method alias_name, base_method def #{base_method}(*opts) - send :#{extend_method}, *opts - send :#{alias_name}, *opts - end + __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 @@ -241,13 +328,19 @@ module IRB module_eval %[ alias_method alias_name, base_method def #{base_method}(*opts) - send :#{alias_name}, *opts - send :#{extend_method}, *opts - end + __send__ :#{alias_name}, *opts + __send__ :#{extend_method}, *opts + end ] end - # return #{prefix}#{name}#{postfix}<num> + # 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) @@ -255,10 +348,9 @@ module IRB return base_name if same_methods.empty? no = same_methods.size while !same_methods.include?(alias_name = base_name + no) - no += 1 + no += 1 end alias_name end end end - diff --git a/lib/irb/frame.rb b/lib/irb/frame.rb index f0b0a9abf3..de54a98f1b 100644 --- a/lib/irb/frame.rb +++ b/lib/irb/frame.rb @@ -1,60 +1,79 @@ +# frozen_string_literal: false # -# frame.rb - +# frame.rb - # $Release Version: 0.9$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(Nihon Rational Software Co.,Ltd) # # -- # -# # - -require "e2mmap" +# module IRB class Frame - extend Exception2MessageMapper - def_exception :FrameOverflow, "frame overflow" - def_exception :FrameUnderflow, "frame underflow" + class FrameOverflow < StandardError + def initialize + super("frame overflow") + end + end + class FrameUnderflow < StandardError + def initialize + super("frame underflow") + end + end + # Default number of stack frames INIT_STACK_TIMES = 3 + # Default number of frames offset CALL_STACK_OFFSET = 3 + # Creates a new stack frame def initialize @frames = [TOPLEVEL_BINDING] * INIT_STACK_TIMES end + # Used by Kernel#set_trace_func to register each event in the call stack def trace_func(event, file, line, id, binding) case event when 'call', 'class' - @frames.push binding + @frames.push binding when 'return', 'end' - @frames.pop + @frames.pop end end + # Returns the +n+ number of frames on the call stack from the last frame + # initialized. + # + # Raises FrameUnderflow if there are no frames in the given stack range. def top(n = 0) bind = @frames[-(n + CALL_STACK_OFFSET)] - Fail FrameUnderflow unless bind + fail FrameUnderflow unless bind bind end + # Returns the +n+ number of frames on the call stack from the first frame + # initialized. + # + # Raises FrameOverflow if there are no frames in the given stack range. def bottom(n = 0) bind = @frames[n] - Fail FrameOverflow unless bind + fail FrameOverflow unless bind bind end - # singleton functions + # Convenience method for Frame#bottom def Frame.bottom(n = 0) @backtrace.bottom(n) end + # Convenience method for Frame#top def Frame.top(n = 0) @backtrace.top(n) end + # Returns the binding context of the caller from the last frame initialized def Frame.sender eval "self", @backtrace.top end diff --git a/lib/irb/help.rb b/lib/irb/help.rb index f091999bd1..3eeaf841b0 100644 --- a/lib/irb/help.rb +++ b/lib/irb/help.rb @@ -1,33 +1,36 @@ +# frozen_string_literal: false # -# irb/help.rb - print usase module -# $Release Version: 0.9.5$ +# irb/help.rb - print usage module +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # 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 lc = IRB.conf[:LC_MESSAGES] path = lc.find("irb/help-message") space_line = false - File.foreach(path) do - |l| - if /^\s*$/ =~ l - lc.puts l unless space_line - space_line = true - next + IRB::MagicFile.open(path){|f| + f.each_line do |l| + if /^\s*$/ =~ l + lc.puts l unless space_line + space_line = true + next + end + space_line = false + + l.sub!(/#.*$/, "") + next if /^\s*$/ =~ l + lc.puts l end - space_line = false - - l.sub!(/#.*$/, "") - next if /^\s*$/ =~ l - lc.puts l - end + } end end - diff --git a/lib/irb/init.rb b/lib/irb/init.rb index db22ca639b..d2baee2017 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -1,27 +1,27 @@ +# frozen_string_literal: false # # irb/init.rb - irb initialize module -# $Release Version: 0.9.5$ +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -module IRB +module IRB # :nodoc: # initialize config - def IRB.setup(ap_path) + def IRB.setup(ap_path, argv: ::ARGV) IRB.init_config(ap_path) IRB.init_error - IRB.parse_opts + IRB.parse_opts(argv: argv) IRB.run_config IRB.load_modules unless @CONF[:PROMPT][@CONF[:PROMPT_MODE]] - IRB.fail(UndefinedPromptMode, @CONF[:PROMPT_MODE]) + fail UndefinedPromptMode, @CONF[:PROMPT_MODE] end end @@ -43,159 +43,288 @@ module IRB @CONF[:LOAD_MODULES] = [] @CONF[:IRB_RC] = nil - @CONF[:MATH_MODE] = false - @CONF[:USE_READLINE] = false unless defined?(ReadlineInputMethod) - @CONF[:INSPECT_MODE] = nil + @CONF[:USE_SINGLELINE] = false unless defined?(ReadlineInputMethod) + @CONF[:USE_COLORIZE] = !ENV['NO_COLOR'] + @CONF[:USE_AUTOCOMPLETE] = true + @CONF[:INSPECT_MODE] = true @CONF[:USE_TRACER] = false @CONF[:USE_LOADER] = false @CONF[:IGNORE_SIGINT] = true @CONF[:IGNORE_EOF] = false + @CONF[:EXTRA_DOC_DIRS] = [] @CONF[:ECHO] = nil + @CONF[:ECHO_ON_ASSIGNMENT] = nil @CONF[:VERBOSE] = nil @CONF[:EVAL_HISTORY] = nil - @CONF[:SAVE_HISTORY] = nil + @CONF[:SAVE_HISTORY] = 1000 @CONF[:BACK_TRACE_LIMIT] = 16 @CONF[:PROMPT] = { :NULL => { - :PROMPT_I => nil, - :PROMPT_N => nil, - :PROMPT_S => nil, - :PROMPT_C => nil, - :RETURN => "%s\n" + :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* ", - :RETURN => "=> %s\n" + :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" }, :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" + :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 => nil, - :PROMPT_C => "?> ", - :RETURN => "=> %s\n" + :PROMPT_I => ">> ", + :PROMPT_N => ">> ", + :PROMPT_S => "%l> ", + :PROMPT_C => "?> ", + :RETURN => "=> %s\n" }, :INF_RUBY => { - :PROMPT_I => "%N(%m):%03n:%i> ", -# :PROMPT_N => "%N(%m):%03n:%i> ", - :PROMPT_N => nil, - :PROMPT_S => nil, - :PROMPT_C => nil, - :RETURN => "%s\n", - :AUTO_INDENT => true + :PROMPT_I => "%N(%m):%03n:%i> ", + :PROMPT_N => nil, + :PROMPT_S => nil, + :PROMPT_C => nil, + :RETURN => "%s\n", + :AUTO_INDENT => true }, :XMP => { - :PROMPT_I => nil, - :PROMPT_N => nil, - :PROMPT_S => nil, - :PROMPT_C => nil, - :RETURN => " ==>%s\n" + :PROMPT_I => nil, + :PROMPT_N => nil, + :PROMPT_S => nil, + :PROMPT_C => nil, + :RETURN => " ==>%s\n" } } @CONF[:PROMPT_MODE] = (STDIN.tty? ? :DEFAULT : :NULL) - @CONF[:AUTO_INDENT] = false + @CONF[:AUTO_INDENT] = true - @CONF[:CONTEXT_MODE] = 3 # use binding in function on TOPLEVEL_BINDING + @CONF[:CONTEXT_MODE] = 4 # use a copy of TOPLEVEL_BINDING @CONF[:SINGLE_IRB] = false -# @CONF[:LC_MESSAGES] = "en" + @CONF[:MEASURE] = false + @CONF[:MEASURE_PROC] = {} + @CONF[:MEASURE_PROC][:TIME] = proc { |context, code, line_no, &block| + time = Time.now + result = block.() + now = Time.now + puts 'processing time: %fs' % (now - time) if IRB.conf[:MEASURE] + result + } + # arg can be either a symbol for the mode (:cpu, :wall, ..) or a hash for + # a more complete configuration. + # See https://github.com/tmm1/stackprof#all-options. + @CONF[:MEASURE_PROC][:STACKPROF] = proc { |context, code, line_no, arg, &block| + return block.() unless IRB.conf[:MEASURE] + success = false + begin + require 'stackprof' + success = true + rescue LoadError + puts 'Please run "gem install stackprof" before measuring by StackProf.' + end + if success + result = nil + arg = { mode: arg || :cpu } unless arg.is_a?(Hash) + stackprof_result = StackProf.run(**arg) do + result = block.() + end + case stackprof_result + when File + puts "StackProf report saved to #{stackprof_result.path}" + when Hash + StackProf::Report.new(stackprof_result).print_text + else + puts "Stackprof ran with #{arg.inspect}" + end + result + else + block.() + end + } + @CONF[:MEASURE_CALLBACKS] = [] + @CONF[:LC_MESSAGES] = Locale.new - - @CONF[:DEBUG_LEVEL] = 1 + + @CONF[:AT_EXIT] = [] + end + + def IRB.set_measure_callback(type = nil, arg = nil, &block) + added = nil + if type + type_sym = type.upcase.to_sym + if IRB.conf[:MEASURE_PROC][type_sym] + added = [type_sym, IRB.conf[:MEASURE_PROC][type_sym], arg] + end + elsif IRB.conf[:MEASURE_PROC][:CUSTOM] + added = [:CUSTOM, IRB.conf[:MEASURE_PROC][:CUSTOM], arg] + elsif block_given? + added = [:BLOCK, block, arg] + found = IRB.conf[:MEASURE_CALLBACKS].find{ |m| m[0] == added[0] && m[2] == added[2] } + if found + found[1] = block + return added + else + IRB.conf[:MEASURE_CALLBACKS] << added + return added + end + else + added = [:TIME, IRB.conf[:MEASURE_PROC][:TIME], arg] + end + if added + found = IRB.conf[:MEASURE_CALLBACKS].find{ |m| m[0] == added[0] && m[2] == added[2] } + if found + # already added + nil + else + IRB.conf[:MEASURE_CALLBACKS] << added if added + added + end + else + nil + end + end + + def IRB.unset_measure_callback(type = nil) + if type.nil? + IRB.conf[:MEASURE_CALLBACKS].clear + else + type_sym = type.upcase.to_sym + IRB.conf[:MEASURE_CALLBACKS].reject!{ |t, | t == type_sym } + end end def IRB.init_error @CONF[:LC_MESSAGES].load("irb/error.rb") end - FEATURE_IOPT_CHANGE_VERSION = "1.9.0" - # option analyzing - def IRB.parse_opts + def IRB.parse_opts(argv: ::ARGV) load_path = [] - while opt = ARGV.shift + while opt = argv.shift case opt when "-f" - @CONF[:RC] = false - when "-m" - @CONF[:MATH_MODE] = true + @CONF[:RC] = false when "-d" - $DEBUG = true + $DEBUG = true + $VERBOSE = true + when "-w" + Warning[:deprecated] = $VERBOSE = true + when /^-W(.+)?/ + opt = $1 || argv.shift + case opt + when "0" + $VERBOSE = nil + when "1" + $VERBOSE = false + else + Warning[:deprecated] = $VERBOSE = true + end when /^-r(.+)?/ - opt = $1 || ARGV.shift - @CONF[:LOAD_MODULES].push opt if opt + opt = $1 || argv.shift + @CONF[:LOAD_MODULES].push opt if opt when /^-I(.+)?/ - opt = $1 || ARGV.shift - load_path.concat(opt.split(File::PATH_SEPARATOR)) if opt - when /^-K(.)/ - $KCODE = $1 + opt = $1 || argv.shift + load_path.concat(opt.split(File::PATH_SEPARATOR)) if opt + when '-U' + set_encoding("UTF-8", "UTF-8") + when /^-E(.+)?/, /^--encoding(?:=(.+))?/ + opt = $1 || argv.shift + set_encoding(*opt.split(':', 2)) when "--inspect" - @CONF[:INSPECT_MODE] = true + if /^-/ !~ argv.first + @CONF[:INSPECT_MODE] = argv.shift + else + @CONF[:INSPECT_MODE] = true + end when "--noinspect" - @CONF[:INSPECT_MODE] = false - when "--readline" - @CONF[:USE_READLINE] = true - when "--noreadline" - @CONF[:USE_READLINE] = false + @CONF[:INSPECT_MODE] = false + when "--singleline", "--readline", "--legacy" + @CONF[:USE_SINGLELINE] = true + when "--nosingleline", "--noreadline" + @CONF[:USE_SINGLELINE] = false + when "--multiline", "--reidline" + @CONF[:USE_MULTILINE] = true + when "--nomultiline", "--noreidline" + @CONF[:USE_MULTILINE] = false + when /^--extra-doc-dir(?:=(.+))?/ + opt = $1 || argv.shift + @CONF[:EXTRA_DOC_DIRS] << opt when "--echo" - @CONF[:ECHO] = true + @CONF[:ECHO] = true when "--noecho" - @CONF[:ECHO] = false + @CONF[:ECHO] = false + when "--echo-on-assignment" + @CONF[:ECHO_ON_ASSIGNMENT] = true + when "--noecho-on-assignment" + @CONF[:ECHO_ON_ASSIGNMENT] = false + when "--truncate-echo-on-assignment" + @CONF[:ECHO_ON_ASSIGNMENT] = :truncate when "--verbose" - @CONF[:VERBOSE] = true + @CONF[:VERBOSE] = true when "--noverbose" - @CONF[:VERBOSE] = false - when "--prompt-mode", "--prompt" - prompt_mode = ARGV.shift.upcase.tr("-", "_").intern - @CONF[:PROMPT_MODE] = prompt_mode + @CONF[:VERBOSE] = false + when "--colorize" + @CONF[:USE_COLORIZE] = true + when "--nocolorize" + @CONF[:USE_COLORIZE] = false + when "--autocomplete" + @CONF[:USE_AUTOCOMPLETE] = true + when "--noautocomplete" + @CONF[:USE_AUTOCOMPLETE] = false + 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 + @CONF[:PROMPT_MODE] = :NULL when "--inf-ruby-mode" - @CONF[:PROMPT_MODE] = :INF_RUBY + @CONF[:PROMPT_MODE] = :INF_RUBY when "--sample-book-mode", "--simple-prompt" - @CONF[:PROMPT_MODE] = :SIMPLE + @CONF[:PROMPT_MODE] = :SIMPLE when "--tracer" - @CONF[:USE_TRACER] = true - when "--back-trace-limit" - @CONF[:BACK_TRACE_LIMIT] = ARGV.shift.to_i - when "--context-mode" - @CONF[:CONTEXT_MODE] = ARGV.shift.to_i + @CONF[:USE_TRACER] = true + when /^--back-trace-limit(?:=(.+))?/ + @CONF[:BACK_TRACE_LIMIT] = ($1 || argv.shift).to_i + when /^--context-mode(?:=(.+))?/ + @CONF[:CONTEXT_MODE] = ($1 || argv.shift).to_i when "--single-irb" - @CONF[:SINGLE_IRB] = true - when "--irb_debug" - @CONF[:DEBUG_LEVEL] = ARGV.shift.to_i + @CONF[:SINGLE_IRB] = true when "-v", "--version" - print IRB.version, "\n" - exit 0 + print IRB.version, "\n" + exit 0 when "-h", "--help" - require "irb/help" - IRB.print_usage - exit 0 + require_relative "help" + IRB.print_usage + exit 0 + when "--" + if opt = argv.shift + @CONF[:SCRIPT] = opt + $0 = opt + end + break when /^-/ - IRB.fail UnrecognizedSwitch, opt + fail UnrecognizedSwitch, opt else - @CONF[:SCRIPT] = opt - $0 = opt - break + @CONF[:SCRIPT] = opt + $0 = opt + break end end - if RUBY_VERSION >= FEATURE_IOPT_CHANGE_VERSION - load_path.collect! do |path| - /\A\.\// =~ path ? path : File.expand_path(path) - end + + load_path.collect! do |path| + /\A\.\// =~ path ? path : File.expand_path(path) end $LOAD_PATH.unshift(*load_path) end @@ -204,14 +333,14 @@ module IRB def IRB.run_config if @CONF[:RC] begin - load rc_file + load rc_file rescue LoadError, Errno::ENOENT - rescue - print "load error: #{rc_file}\n" - print $!.class, ": ", $!, "\n" - for err in $@[0, $@.size - 2] - print "\t", err, "\n" - end + rescue # StandardError, ScriptError + print "load error: #{rc_file}\n" + print $!.class, ": ", $!, "\n" + for err in $@[0, $@.size - 2] + print "\t", err, "\n" + end end end end @@ -220,40 +349,74 @@ module IRB 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 + @CONF[:RC_NAME_GENERATOR] ||= rcgen + if File.exist?(rcgen.call(IRBRC_EXT)) + @CONF[:RC_NAME_GENERATOR] = rcgen + break + end end end - @CONF[:RC_NAME_GENERATOR].call ext + case rc_file = @CONF[:RC_NAME_GENERATOR].call(ext) + when String + return rc_file + else + fail IllegalRCNameGenerator + end 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} + 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}"} + yield proc{|rc| home+"/.irb#{rc}"} end - home = Dir.pwd - yield proc{|rc| home+"/.irb#{rc}"} - yield proc{|rc| home+"/irb#{rc.sub(/\A_?/, '.')}"} - yield proc{|rc| home+"/_irb#{rc}"} - yield proc{|rc| home+"/$irb#{rc}"} + 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}"} end # loading modules def IRB.load_modules for m in @CONF[:LOAD_MODULES] begin - require m - rescue - print $@[0], ":", $!.class, ": ", $!, "\n" + require m + rescue LoadError => err + warn "#{err.class}: #{err}", uplevel: 0 end end end + + DefaultEncodings = Struct.new(:external, :internal) + class << IRB + private + def set_encoding(extern, intern = nil, override: true) + verbose, $VERBOSE = $VERBOSE, nil + Encoding.default_external = extern unless extern.nil? || extern.empty? + Encoding.default_internal = intern unless intern.nil? || intern.empty? + [$stdin, $stdout, $stderr].each do |io| + io.set_encoding(extern, intern) + end + if override + @CONF[:LC_MESSAGES].instance_variable_set(:@override_encoding, extern) + else + @CONF[:LC_MESSAGES].instance_variable_set(:@encoding, extern) + end + ensure + $VERBOSE = verbose + end + end end diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index bfb90fa59a..64276e61be 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -1,120 +1,469 @@ +# frozen_string_literal: false # # irb/input-method.rb - input methods used irb -# $Release Version: 0.9.5$ +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # +# +require_relative 'src_encoding' +require_relative 'magic-file' +require_relative 'completion' +require 'io/console' +require 'reline' +require 'rdoc' + module IRB - # - # InputMethod - # StdioInputMethod - # FileInputMethod - # (ReadlineInputMethod) - # - STDIN_FILE_NAME = "(line)" + STDIN_FILE_NAME = "(line)" # :nodoc: class InputMethod - @RCS_ID='-$Id$-' + # 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 + # The irb prompt associated with this input method attr_accessor :prompt - + + # Reads the next line from this input method. + # + # See IO#gets for more information. def gets - IRB.fail NotImplementedError, "gets" + fail NotImplementedError, "gets" end public :gets - def readable_atfer_eof? + def winsize + if instance_variable_defined?(:@stdout) + @stdout.winsize + else + [24, 80] + end + 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? false end + + # For debug message + def inspect + 'Abstract InputMethod' + end end - + 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 => "-") + @stdout = IO.open(STDOUT.to_i, 'w', :external_encoding => IRB.conf[:LC_MESSAGES].encoding, :internal_encoding => "-") end + # Reads the next line from this input method. + # + # See IO#gets for more information. def gets print @prompt - @line[@line_no += 1] = $stdin.gets + line = @stdin.gets + @line[@line_no += 1] = line 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? - $stdin.eof? + if @stdin.wait_readable(0.00001) + c = @stdin.getc + result = c.nil? ? true : false + @stdin.ungetc(c) unless c.nil? + result + else # buffer is empty + false + end end - def readable_atfer_eof? + # 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 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 + 'StdioInputMethod' + end end - + + # Use a File for IO with irb, see InputMethod class FileInputMethod < InputMethod + class << self + def open(file, &block) + begin + io = new(file) + block.call(io) + ensure + io&.close + end + end + end + + # Creates a new input method object def initialize(file) super - @io = open(file) + @io = IRB::MagicFile.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. + # + # See IO#eof? for more information. def eof? - @io.eof? + @io.closed? || @io.eof? end + # Reads the next line from this input method. + # + # See IO#gets for more information. def gets print @prompt - l = @io.gets -# print @prompt, l - l + @io.gets + end + + # The external encoding for standard input. + def encoding + @external_encoding + end + + # For debug message + def inspect + 'FileInputMethod' + end + + def close + @io.close end end begin - require "readline" class ReadlineInputMethod < InputMethod - include Readline + def self.initialize_readline + require "readline" + rescue LoadError + else + include ::Readline + end + + # Creates a new input method object using Readline def initialize - super + self.class.initialize_readline + if Readline.respond_to?(:encoding_system_needs) + IRB.__send__(:set_encoding, Readline.encoding_system_needs.name, override: false) + end + super + + @line_no = 0 + @line = [] + @eof = false - @line_no = 0 - @line = [] - @eof = false + @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 => "-") + + 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 + # Reads the next line from this input method. + # + # See IO#gets for more information. def gets - if l = readline(@prompt, false) + 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 + @line[@line_no += 1] = l + "\n" + else + @eof = true + l + end 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 + @eof end - def readable_atfer_eof? - true + # 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 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] + @line[line_no] + end + + # The external encoding for standard input. + def encoding + @stdin.external_encoding + 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 + end + + class ReidlineInputMethod < InputMethod + include Reline + + # Creates a new input method object using Reline + def initialize + IRB.__send__(:set_encoding, Reline.encoding_system_needs.name, override: false) + super + + @line_no = 0 + @line = [] + @eof = false + + @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 => "-") + + if Reline.respond_to?("basic_word_break_characters=") + Reline.basic_word_break_characters = IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS + end + Reline.completion_append_character = nil + Reline.completer_quote_characters = '' + Reline.completion_proc = IRB::InputCompletor::CompletionProc + 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) + end + else + proc do |output| + Reline::Unicode.escape_for_print(output) + end + end + Reline.dig_perfect_match_proc = IRB::InputCompletor::PerfectMatchedProc + Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE] + if IRB.conf[:USE_AUTOCOMPLETE] + Reline.add_dialog_proc(:show_doc, SHOW_DOC_DIALOG, Reline::DEFAULT_DIALOG_CONTEXT) + end + end + + def check_termination(&block) + @check_termination_proc = block + end + + def dynamic_prompt(&block) + @prompt_proc = block + end + + def auto_indent(&block) + @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. + ] + + 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) + + options = {} + options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty? + driver = RDoc::RI::Driver.new(options) + + if key.match?(dialog.name) + begin + driver.display_names([name]) + rescue RDoc::RI::Driver::NotFoundError + end + end + + 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 + 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 + 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 + 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') + } + + # Reads the next line from this input method. + # + # See IO#gets for more information. + def gets + Reline.input = @stdin + 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? + @line[@line_no += 1] = l + "\n" + else + @eof = true + l + end + 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 + + # 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 + 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 += " and #{inputrc_path}" if File.exist?(inputrc_path) + str end - rescue LoadError end end diff --git a/lib/irb/inspector.rb b/lib/irb/inspector.rb new file mode 100644 index 0000000000..c2f3b605db --- /dev/null +++ b/lib/irb/inspector.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: false +# +# 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: + + + # Convenience method to create a new Inspector, using the given +inspect+ + # proc, and optional +init+ proc and passes them to Inspector.new + # + # irb(main):001:0> ins = IRB::Inspector(proc{ |v| "omg! #{v}" }) + # irb(main):001:0> IRB.CurrentContext.inspect_mode = ins # => omg! #<IRB::Inspector:0x007f46f7ba7d28> + # irb(main):001:0> "what?" #=> omg! what? + # + def IRB::Inspector(inspect, init = nil) + Inspector.new(inspect, init) + end + + # An irb inspector + # + # In order to create your own custom inspector there are two things you + # should be aware of: + # + # Inspector uses #inspect_value, or +inspect_proc+, for output of return values. + # + # This also allows for an optional #init+, or +init_proc+, which is called + # when the inspector is activated. + # + # Knowing this, you can create a rudimentary inspector as follows: + # + # irb(main):001:0> ins = IRB::Inspector.new(proc{ |v| "omg! #{v}" }) + # irb(main):001:0> IRB.CurrentContext.inspect_mode = ins # => omg! #<IRB::Inspector:0x007f46f7ba7d28> + # irb(main):001:0> "what?" #=> omg! what? + # + class Inspector + # Default inspectors available to irb, this includes: + # + # +:pp+:: Using Kernel#pretty_inspect + # +:yaml+:: Using YAML.dump + # +:marshal+:: Using Marshal.dump + INSPECTORS = {} + + # 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} + end + + # Example + # + # Inspector.def_inspector(key, init_p=nil){|v| v.inspect} + # Inspector.def_inspector([key1,..], init_p=nil){|v| v.inspect} + # Inspector.def_inspector(key, inspector) + # Inspector.def_inspector([key1,...], inspector) + def self.def_inspector(key, arg=nil, &block) + if block_given? + inspector = IRB::Inspector(block, arg) + else + inspector = arg + end + + case key + when Array + for k in key + def_inspector(k, inspector) + end + when Symbol + INSPECTORS[key] = inspector + INSPECTORS[key.to_s] = inspector + when String + INSPECTORS[key] = inspector + INSPECTORS[key.intern] = inspector + else + INSPECTORS[key] = inspector + end + end + + # Creates a new inspector object, using the given +inspect_proc+ when + # output return values in irb. + def initialize(inspect_proc, init_proc = nil) + @init = init_proc + @inspect = inspect_proc + end + + # Proc to call when the inspector is activated, good for requiring + # dependent libraries. + def init + @init.call if @init + end + + # 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)" + '' + end + end + + Inspector.def_inspector([false, :to_s, :raw]){|v| v.to_s} + Inspector.def_inspector([:p, :inspect]){|v| + result = v.inspect + if IRB.conf[:MAIN_CONTEXT]&.use_colorize? && Color.inspect_colorable?(v) + result = Color.colorize_code(result) + end + result + } + Inspector.def_inspector([true, :pp, :pretty_inspect], proc{require "irb/color_printer"}){|v| + if IRB.conf[:MAIN_CONTEXT]&.use_colorize? + IRB::ColorPrinter.pp(v, '').chomp + else + v.pretty_inspect.chomp + end + } + Inspector.def_inspector([:yaml, :YAML], proc{require "yaml"}){|v| + begin + YAML.dump(v) + rescue + puts "(can't dump yaml. use inspect)" + v.inspect + end + } + + Inspector.def_inspector([:marshal, :Marshal, :MARSHAL, Marshal]){|v| + Marshal.dump(v) + } +end diff --git a/lib/irb/irb.gemspec b/lib/irb/irb.gemspec new file mode 100644 index 0000000000..26d0fb018f --- /dev/null +++ b/lib/irb/irb.gemspec @@ -0,0 +1,40 @@ +begin + require_relative "lib/irb/version" +rescue LoadError + # for Ruby core repository + require_relative "version" +end + +Gem::Specification.new do |spec| + spec.name = "irb" + spec.version = IRB::VERSION + spec.authors = ["Keiju ISHITSUKA"] + spec.email = ["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.files = [ + ".document", + "Gemfile", + "LICENSE.txt", + "README.md", + "Rakefile", + "bin/console", + "bin/setup", + "doc/irb/irb-tools.rd.ja", + "doc/irb/irb.rd.ja", + "exe/irb", + "irb.gemspec", + "man/irb.1", + ] + Dir.glob("lib/**/*") + spec.bindir = "exe" + 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.add_dependency "reline", ">= 0.3.0" +end diff --git a/lib/irb/lc/error.rb b/lib/irb/lc/error.rb index 247596b7fe..798994e92c 100644 --- a/lib/irb/lc/error.rb +++ b/lib/irb/lc/error.rb @@ -1,30 +1,71 @@ +# frozen_string_literal: false # -# irb/lc/error.rb - -# $Release Version: 0.9.5$ +# irb/lc/error.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # -require "e2mmap" +# +# :stopdoc: module IRB - - # exceptions - extend Exception2MessageMapper - def_exception :UnrecognizedSwitch, "Unrecognized switch: %s" - def_exception :NotImplementedError, "Need to define `%s'" - def_exception :CantReturnToNormalMode, "Can't return to normal mode." - def_exception :IllegalParameter, "Illegal parameter(%s)." - def_exception :IrbAlreadyDead, "Irb is already dead." - def_exception :IrbSwitchedToCurrentThread, "Switched to current thread." - def_exception :NoSuchJob, "No such job(%s)." - def_exception :CantShiftToMultiIrbMode, "Can't shift to multi irb mode." - def_exception :CantChangeBinding, "Can't change binding to (%s)." - def_exception :UndefinedPromptMode, "Undefined prompt mode(%s)." - + class UnrecognizedSwitch < StandardError + def initialize(val) + 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.") + end + end + class IllegalParameter < StandardError + def initialize(val) + super("Invalid parameter(#{val}).") + end + end + class IrbAlreadyDead < StandardError + def initialize + super("Irb is already dead.") + end + end + class IrbSwitchedToCurrentThread < StandardError + def initialize + super("Switched to current thread.") + end + end + class NoSuchJob < StandardError + def initialize(val) + 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}).") + end + end + class UndefinedPromptMode < StandardError + def initialize(val) + super("Undefined prompt mode(#{val}).") + end + end + class IllegalRCGenerator < StandardError + def initialize + super("Define illegal RC_NAME_GENERATOR.") + end + end end - +# :startdoc: diff --git a/lib/irb/lc/help-message b/lib/irb/lc/help-message index 32087d113c..939dc38975 100644 --- a/lib/irb/lc/help-message +++ b/lib/irb/lc/help-message @@ -1,35 +1,61 @@ +# -*- coding: utf-8 -*- # -# irb/lc/help-message.rb - -# $Release Version: 0.9.5$ +# irb/lc/help-message.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # Usage: irb.rb [options] [programfile] [arguments] - -f Suppress read of ~/.irbrc - -m Bc mode (load mathn, fraction or matrix are available) + -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 - --inspect Use `inspect' for output (default except for bc mode) - --noinspect Don't use inspect for output - --readline Use Readline extension module - --noreadline Don't use Readline extension module - --prompt prompt-mode - --prompt-mode prompt-mode - Switch prompt mode. Pre-defined prompt modes are - `default', `simple', `xmp' and `inf-ruby' - --inf-ruby-mode Use prompt appropriate for inf-ruby-mode on emacs. - Suppresses --readline. - --simple-prompt Simple prompt mode - --noprompt No prompt mode - --tracer Display trace for each execution of commands. + -U Same as `ruby -U` + -E enc Same as `ruby -E` + -w Same as `ruby -w` + -W[level=2] 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 + --echo-on-assignment + Show result on assignment + --noecho-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' + --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 + --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. - --irb_debug n Set internal debug level to n (not for popular use) + 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 diff --git a/lib/irb/lc/ja/encoding_aliases.rb b/lib/irb/lc/ja/encoding_aliases.rb new file mode 100644 index 0000000000..c534bf0fef --- /dev/null +++ b/lib/irb/lc/ja/encoding_aliases.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: false +# :stopdoc: +module IRB + class Locale + @@legacy_encoding_alias_map = { + 'ujis' => Encoding::EUC_JP, + 'euc' => Encoding::EUC_JP + }.freeze + end +end +# :startdoc: diff --git a/lib/irb/lc/ja/error.rb b/lib/irb/lc/ja/error.rb index 4c2fb3b839..31ebb3b5f0 100644 --- a/lib/irb/lc/ja/error.rb +++ b/lib/irb/lc/ja/error.rb @@ -1,27 +1,72 @@ -# -# irb/lc/ja/error.rb - -# $Release Version: 0.9.5$ +# -*- coding: utf-8 -*- +# frozen_string_literal: false +# irb/lc/ja/error.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # -require "e2mmap" +# +# :stopdoc: module IRB - # exceptions - extend Exception2MessageMapper - def_exception :UnrecognizedSwitch, 'スイッチ(%s)が分りません' - def_exception :NotImplementedError, '`%s\'の定義が必要です' - def_exception :CantReturnToNormalMode, 'Normalモ・踉札匹北瓩譴泙擦鹿粡寂蝟赱齠洲粤讌将粤聨纔竇頸蜿塞跛繚瘡倚鱇辣鬪パラメ・踉札┘鶇が間違っています.' - def_exception :IrbAlreadyDead, 'Irbは既に死んでいます.' - def_exception :IrbSwitchedToCurrentThread, 'カレントスレッドに切り替わりました.' - def_exception :NoSuchJob, 'そのよう瘢雹なジョブ(%s)はありません.' - def_exception :CantShiftToMultiIrbMode, 'multi-irb modeに移れません.' - def_exception :CantChangeBinding, 'バインディング(%s)に変更できません.' - def_exception :UndefinedPromptMode, 'プロンプトモ・踉札┘鶇は定義されていません.' + class UnrecognizedSwitch < StandardError + def initialize(val) + super("(#{val})") + end + end + class NotImplementedError < StandardError + def initialize(val) + super("`#{val}'") + end + end + class CantReturnToNormalMode < StandardError + def initialize + super("Normal.") + end + end + class IllegalParameter < StandardError + def initialize(val) + super("(#{val}).") + end + end + class IrbAlreadyDead < StandardError + def initialize + super("Irb.") + end + end + class IrbSwitchedToCurrentThread < StandardError + def initialize + super(".") + end + end + class NoSuchJob < StandardError + def initialize(val) + super("(#{val}).") + end + end + class CantShiftToMultiIrbMode < StandardError + def initialize + super("multi-irb mode.") + end + end + class CantChangeBinding < StandardError + def initialize(val) + super("(#{val}).") + end + end + class UndefinedPromptMode < StandardError + def initialize(val) + super("(#{val}).") + end + end + class IllegalRCGenerator < StandardError + def initialize + super("RC_NAME_GENERATOR.") + end + end end +# :startdoc: +# vim:fileencoding=utf-8 diff --git a/lib/irb/lc/ja/help-message b/lib/irb/lc/ja/help-message index debbfe9355..238535afb7 100644 --- a/lib/irb/lc/ja/help-message +++ b/lib/irb/lc/ja/help-message @@ -1,36 +1,57 @@ -# -# irb/lc/ja/help-message.rb - -# $Release Version: 0.9.5$ +# -*- coding: utf-8 -*- +# irb/lc/ja/help-message.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # Usage: irb.rb [options] [programfile] [arguments] - -f ~/.irbrc を読み込まない. - -m bcモ・踉札ぢ分数, 行列の計算ができる) - -d $DEBUG をtrueにする(ruby -d と同じ) - -r load-module ruby -r と同じ. - -I path $LOAD_PATH に path を追加する. - --inspect 結果出力にinspectを用いる(bcモ・踉札桧奮阿魯妊侫襯蝟昭粡竚癈鷭т繻Ь濶銖鞳笏 結果出力にinspectを用いない. - --readline readlineライブラリを利用する. - --noreadline readlineライブラリを利用しない. + -f ~/.irbrc . + -d $DEBUG true(ruby -d ) + -r load-module ruby -r . + -I path $LOAD_PATH path . + -U ruby -U . + -E enc ruby -E . + -w ruby -w . + -W[level=2] ruby -W . + --context-mode n Binding + 0 3 . + --echo (). + --noecho . + --inspect inspect. + --noinspect inspect. + --multiline . + --nomultiline . + --singleline . + --nosingleline . + --colorize . + --nocolorize . + --autocomplete . + --noautocomplete . --prompt prompt-mode/--prompt-mode prompt-mode - プロンプトモ・踉札匹鮴畋悗┐泙現在定義されているプ - ロンプトモ・踉札匹粤聲蜊韭絳逅蜴罩鴣磔が - 用意されています. - --inf-ruby-mode emacsのinf-ruby-mode用のプロンプト表┐鮃圓覆逅特 - に指定がない限り, readlineライブラリは使わなくなる. - --simple-prompt 非錣縫轡鵐廛襪淵廛蹈鵐廛箸鰺僂い襯癲ドです. - --noprompt プロンプト表┐鮃圓覆錣覆蝟昭粡竚癈鷭т繻Ь鱇竇コマンド孫垰縫肇譟スを行なう瘢雹. + . + , default, simple, xmp, inf-ruby + . + --inf-ruby-mode emacsinf-ruby-mode. + , + . + --sample-book-mode/--simple-prompt + . + --noprompt . + --single-irb irb self + irb . + --tracer . --back-trace-limit n - バックトレ・踉札紘拾踉雑をバックトレ・踉札垢瞭後ろ - からnだけ行なう瘢雹. デフォルトは16 - --irb_debug n irbのデバッグデバッグレベルをnに設定する(利用しな - い方が無難でしょう瘢雹). - -v, --version irbのバ・踉札献腑鵑鯢拾踉雑する + n, + n. 16 + + --verbose . + --noverbose (). + -v, --version irb. + -h, --help irb . + -- . + +# vim:fileencoding=utf-8 diff --git a/lib/irb/locale.rb b/lib/irb/locale.rb index 5ed9f54507..bb44b41002 100644 --- a/lib/irb/locale.rb +++ b/lib/irb/locale.rb @@ -1,51 +1,59 @@ +# frozen_string_literal: false # # irb/locale.rb - internationalization module -# $Release Version: 0.9.5$ +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # - -autoload :Kconv, "kconv" - -module IRB +# +module IRB # :nodoc: class Locale - @RCS_ID='-$Id$-' - JPDefaultLocale = "ja" + LOCALE_NAME_RE = %r[ + (?<language>[[:alpha:]]{2,3}) + (?:_ (?<territory>[[:alpha:]]{2,3}) )? + (?:\. (?<codeset>[^@]+) )? + (?:@ (?<modifier>.*) )? + ]x LOCALE_DIR = "/lc/" + @@legacy_encoding_alias_map = {}.freeze + @@loaded = [] + def initialize(locale = nil) - @lang = locale || ENV["IRB_LANG"] || ENV["LC_MESSAGES"] || ENV["LC_ALL"] || ENV["LANG"] || "C" + @override_encoding = nil + @lang = @territory = @encoding_name = @modifier = nil + @locale = locale || ENV["IRB_LANG"] || ENV["LC_MESSAGES"] || ENV["LC_ALL"] || ENV["LANG"] || "C" + if m = LOCALE_NAME_RE.match(@locale) + @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] + warn(("%s is obsolete. use %s" % ["#{@lang}_#{@territory}.#{@encoding_name}", "#{@lang}_#{@territory}.#{@encoding.name}"]), uplevel: 1) + end + @encoding = Encoding.find(@encoding_name) rescue nil + end + end + @encoding ||= (Encoding.find('locale') rescue Encoding::ASCII_8BIT) end - attr_reader :lang + attr_reader :lang, :territory, :modifier - def lc2kconv(lang) - case lang - when "ja_JP.ujis", "ja_JP.euc", "ja_JP.eucJP" - Kconv::EUC - when "ja_JP.sjis", "ja_JP.SJIS" - Kconv::SJIS - when /ja_JP.utf-?8/i - Kconv::UTF8 - end + def encoding + @override_encoding || @encoding end - private :lc2kconv def String(mes) mes = super(mes) - case @lang - when /^ja/ - mes = Kconv::kconv(mes, lc2kconv(@lang)) + if encoding + mes.encode(encoding, undef: :replace) else - mes + mes end - mes end def format(*opts) @@ -81,104 +89,103 @@ module IRB case file when /\.rb$/ - begin - load(file, priv) - $".push file - return true - rescue LoadError - end + begin + load(file, priv) + $".push file + return true + rescue LoadError + end when /\.(so|o|sl)$/ - return super + return super end begin - load(f = file + ".rb") - $".push f #" - return true + load(f = file + ".rb") + $".push f #" + return true rescue LoadError - return ruby_require(file) + return ruby_require(file) end end alias toplevel_load load - + def load(file, priv=nil) + found = find(file) + if found + unless @@loaded.include?(found) + @@loaded << found # cache + return real_load(found, priv) + end + else + raise LoadError, "No such file to load -- #{file}" + end + end + + def find(file , paths = $:) dir = File.dirname(file) dir = "" if dir == "." base = File.basename(file) - if /^ja(_JP)?$/ =~ @lang - back, @lang = @lang, "C" - end - begin - if dir[0] == ?/ #/ - lc_path = search_file(dir, base) - return real_load(lc_path, priv) if lc_path - end - - for path in $: - lc_path = search_file(path + "/" + dir, base) - return real_load(lc_path, priv) if lc_path - end - ensure - @lang = back if back + if dir.start_with?('/') + return each_localized_path(dir, base).find{|full_path| File.readable? full_path} + else + return search_file(paths, dir, base) end - raise LoadError, "No such file to load -- #{file}" - end + end + private def real_load(path, priv) - src = self.String(File.read(path)) + src = MagicFile.open(path){|f| f.read} if priv - eval("self", TOPLEVEL_BINDING).extend(Module.new {eval(src, nil, path)}) + eval("self", TOPLEVEL_BINDING).extend(Module.new {eval(src, nil, path)}) else - eval(src, TOPLEVEL_BINDING, path) + eval(src, TOPLEVEL_BINDING, path) end end - private :real_load - def find(file , paths = $:) - dir = File.dirname(file) - dir = "" if dir == "." - base = File.basename(file) - if dir[0] == ?/ #/ - return lc_path = search_file(dir, base) - else - for path in $: - if lc_path = search_file(path + "/" + dir, base) - return lc_path - end - end + # @param paths load paths in which IRB find a localized file. + # @param dir directory + # @param file basename to be localized + # + # typically, for the parameters and a <path> in paths, it searches + # <path>/<dir>/<locale>/<file> + def search_file(lib_paths, dir, file) + each_localized_path(dir, file) do |lc_path| + lib_paths.each do |libpath| + full_path = File.join(libpath, lc_path) + return full_path if File.readable?(full_path) + end + redo if defined?(Gem) and Gem.try_activate(lc_path) end nil end - def search_file(path, file) - if File.exist?(p1 = path + lc_path(file, "C")) - if File.exist?(p2 = path + lc_path(file)) - return p2 - else - end - return p1 - else + def each_localized_path(dir, file) + return enum_for(:each_localized_path) unless block_given? + each_sublocale do |lc| + yield lc.nil? ? File.join(dir, LOCALE_DIR, file) : File.join(dir, LOCALE_DIR, lc, file) end - nil end - private :search_file - def lc_path(file = "", lc = @lang) - case lc - when "C" - LOCALE_DIR + file - when /^ja/ - LOCALE_DIR + "ja/" + file - else - LOCALE_DIR + @lang + "/" + file + def each_sublocale + if @lang + if @territory + if @encoding_name + yield "#{@lang}_#{@territory}.#{@encoding_name}@#{@modifier}" if @modifier + yield "#{@lang}_#{@territory}.#{@encoding_name}" + end + yield "#{@lang}_#{@territory}@#{@modifier}" if @modifier + yield "#{@lang}_#{@territory}" + end + if @encoding_name + yield "#{@lang}.#{@encoding_name}@#{@modifier}" if @modifier + yield "#{@lang}.#{@encoding_name}" + end + yield "#{@lang}@#{@modifier}" if @modifier + yield "#{@lang}" end + yield nil end - private :lc_path end end - - - - diff --git a/lib/irb/magic-file.rb b/lib/irb/magic-file.rb new file mode 100644 index 0000000000..34e06d64b3 --- /dev/null +++ b/lib/irb/magic-file.rb @@ -0,0 +1,38 @@ +# 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/notifier.rb b/lib/irb/notifier.rb index c8e66fa859..d0e413dd68 100644 --- a/lib/irb/notifier.rb +++ b/lib/irb/notifier.rb @@ -1,145 +1,236 @@ +# frozen_string_literal: false # -# notifier.rb - optput methods used by irb -# $Release Version: 0.9.5$ +# notifier.rb - output methods used by irb +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -require "e2mmap" -require "irb/output-method" +require_relative "output-method" module IRB + # An output formatter used internally by the lexer. module Notifier - extend Exception2MessageMapper - def_exception :ErrUndefinedNotifier, - "undefined notifier level: %d is specified" - def_exception :ErrUnrecognizedLevel, - "unrecognized notifier level: %s is specified" + class ErrUndefinedNotifier < StandardError + def initialize(val) + super("undefined notifier level: #{val} is specified") + end + end + class ErrUnrecognizedLevel < StandardError + def initialize(val) + super("unrecognized notifier level: #{val} is specified") + end + end + # Define a new Notifier output source, returning a new CompositeNotifier + # with the given +prefix+ and +output_method+. + # + # The optional +prefix+ will be appended to all objects being inspected + # during output, using the given +output_method+ as the output source. If + # no +output_method+ is given, StdioOutputMethod will be used, and all + # expressions will be sent directly to STDOUT without any additional + # formatting. def def_notifier(prefix = "", output_method = StdioOutputMethod.new) CompositeNotifier.new(prefix, output_method) end module_function :def_notifier - - class AbstructNotifier + + # An abstract class, or superclass, for CompositeNotifier and + # LeveledNotifier to inherit. It provides several wrapper methods for the + # OutputMethod object used by the Notifier. + class AbstractNotifier + # Creates a new Notifier object def initialize(prefix, base_notifier) - @prefix = prefix - @base_notifier = base_notifier + @prefix = prefix + @base_notifier = base_notifier end + # The +prefix+ for this Notifier, which is appended to all objects being + # inspected during output. attr_reader :prefix + # A wrapper method used to determine whether notifications are enabled. + # + # Defaults to +true+. def notify? - true + true end + # See OutputMethod#print for more detail. def print(*opts) - @base_notifier.print prefix, *opts if notify? + @base_notifier.print prefix, *opts if notify? end + # See OutputMethod#printn for more detail. def printn(*opts) - @base_notifier.printn prefix, *opts if notify? + @base_notifier.printn prefix, *opts if notify? end + # See OutputMethod#printf for more detail. def printf(format, *opts) - @base_notifier.printf(prefix + format, *opts) if notify? + @base_notifier.printf(prefix + format, *opts) if notify? end + # See OutputMethod#puts for more detail. def puts(*objs) - if notify? - @base_notifier.puts(*objs.collect{|obj| prefix + obj.to_s}) - end + if notify? + @base_notifier.puts(*objs.collect{|obj| prefix + obj.to_s}) + end end + # Same as #ppx, except it uses the #prefix given during object + # initialization. + # See OutputMethod#ppx for more detail. def pp(*objs) - if notify? - @base_notifier.ppx @prefix, *objs - end + if notify? + @base_notifier.ppx @prefix, *objs + end end + # Same as #pp, except it concatenates the given +prefix+ with the #prefix + # given during object initialization. + # + # See OutputMethod#ppx for more detail. def ppx(prefix, *objs) - if notify? - @base_notifier.ppx @prefix+prefix, *objs - end + if notify? + @base_notifier.ppx @prefix+prefix, *objs + end end + # Execute the given block if notifications are enabled. def exec_if - yield(@base_notifier) if notify? + yield(@base_notifier) if notify? end end - class CompositeNotifier<AbstructNotifier + # A class that can be used to create a group of notifier objects with the + # intent of representing a leveled notification system for irb. + # + # This class will allow you to generate other notifiers, and assign them + # the appropriate level for output. + # + # The Notifier class provides a class-method Notifier.def_notifier to + # create a new composite notifier. Using the first composite notifier + # object you create, sibling notifiers can be initialized with + # #def_notifier. + class CompositeNotifier < AbstractNotifier + # Create a new composite notifier object with the given +prefix+, and + # +base_notifier+ to use for output. def initialize(prefix, base_notifier) - super + super - @notifiers = [D_NOMSG] - @level_notifier = D_NOMSG + @notifiers = [D_NOMSG] + @level_notifier = D_NOMSG end + # List of notifiers in the group attr_reader :notifiers + # Creates a new LeveledNotifier in the composite #notifiers group. + # + # The given +prefix+ will be assigned to the notifier, and +level+ will + # be used as the index of the #notifiers Array. + # + # This method returns the newly created instance. def def_notifier(level, prefix = "") - notifier = LeveledNotifier.new(self, level, prefix) - @notifiers[level] = notifier - notifier + notifier = LeveledNotifier.new(self, level, prefix) + @notifiers[level] = notifier + notifier end + # Returns the leveled notifier for this object attr_reader :level_notifier alias level level_notifier + # Sets the leveled notifier for this object. + # + # When the given +value+ is an instance of AbstractNotifier, + # #level_notifier is set to the given object. + # + # When an Integer is given, #level_notifier is set to the notifier at the + # index +value+ in the #notifiers Array. + # + # If no notifier exists at the index +value+ in the #notifiers Array, an + # ErrUndefinedNotifier exception is raised. + # + # An ErrUnrecognizedLevel exception is raised if the given +value+ is not + # found in the existing #notifiers Array, or an instance of + # AbstractNotifier def level_notifier=(value) - case value - when AbstructNotifier - @level_notifier = value - when Integer - l = @notifiers[value] - Notifier.Raise ErrUndefinedNotifer, value unless l - @level_notifier = l - else - Notifier.Raise ErrUnrecognizedLevel, value unless l - end + case value + when AbstractNotifier + @level_notifier = value + when Integer + l = @notifiers[value] + raise ErrUndefinedNotifier, value unless l + @level_notifier = l + else + raise ErrUnrecognizedLevel, value unless l + end end alias level= level_notifier= end - class LeveledNotifier<AbstructNotifier + # A leveled notifier is comparable to the composite group from + # CompositeNotifier#notifiers. + class LeveledNotifier < AbstractNotifier include Comparable + # Create a new leveled notifier with the given +base+, and +prefix+ to + # send to AbstractNotifier.new + # + # The given +level+ is used to compare other leveled notifiers in the + # CompositeNotifier group to determine whether or not to output + # notifications. def initialize(base, level, prefix) - super(prefix, base) - - @level = level + super(prefix, base) + + @level = level end + # The current level of this notifier object attr_reader :level + # Compares the level of this notifier object with the given +other+ + # notifier. + # + # See the Comparable module for more information. def <=>(other) - @level <=> other.level + @level <=> other.level end - + + # Whether to output messages to the output method, depending on the level + # of this notifier object. def notify? - @base_notifier.level >= self + @base_notifier.level >= self end end - class NoMsgNotifier<LeveledNotifier + # NoMsgNotifier is a LeveledNotifier that's used as the default notifier + # when creating a new CompositeNotifier. + # + # This notifier is used as the +zero+ index, or level +0+, for + # CompositeNotifier#notifiers, and will not output messages of any sort. + class NoMsgNotifier < LeveledNotifier + # Creates a new notifier that should not be used to output messages. def initialize - @base_notifier = nil - @level = 0 - @prefix = "" + @base_notifier = nil + @level = 0 + @prefix = "" end + # Ensures notifications are ignored, see AbstractNotifier#notify? for + # more information. def notify? - false + false end end - D_NOMSG = NoMsgNotifier.new + D_NOMSG = NoMsgNotifier.new # :nodoc: end end diff --git a/lib/irb/output-method.rb b/lib/irb/output-method.rb index b9a3a8851e..3fda708cb0 100644 --- a/lib/irb/output-method.rb +++ b/lib/irb/output-method.rb @@ -1,83 +1,90 @@ +# frozen_string_literal: false # -# output-method.rb - optput methods used by irb -# $Release Version: 0.9.5$ +# output-method.rb - output methods used by irb +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # - -require "e2mmap" +# module IRB - # OutputMethod - # StdioOutputMethod - + # 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 - @RCS_ID='-$Id$-' + 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) - IRB.fail NotImplementError, "print" + raise NotImplementedError, "print" end + # Prints the given +opts+, with a newline delimiter. def printn(*opts) print opts.join(" "), "\n" end - # extend printf + # Extends IO#printf to format the given +opts+ for Kernel#sprintf using + # #parse_printf_format def printf(format, *opts) if /(%*)%I/ =~ format - format, opts = parse_printf_format(format, opts) + format, opts = parse_printf_format(format, opts) end print sprintf(format, *opts) end - # % - # <> [#0- +] - # <> (\*|\*[1-9][0-9]*\$|[1-9][0-9]*) - # <>.(\*|\*[1-9][0-9]*\$|[1-9][0-9]*|)? - # #<>(hh|h|l|ll|L|q|j|z|t) - # <>[diouxXeEfgGcsb%] + # Returns an array of the given +format+ and +opts+ to be used by + # Kernel#sprintf, if there was a successful Regexp match in the given + # +format+ from #printf + # + # % + # <flag> [#0- +] + # <minimum field width> (\*|\*[1-9][0-9]*\$|[1-9][0-9]*) + # <precision>.(\*|\*[1-9][0-9]*\$|[1-9][0-9]*|)? + # #<length modifier>(hh|h|l|ll|L|q|j|z|t) + # <conversion specifier>[diouxXeEfgGcsb%] def parse_printf_format(format, opts) return format, opts if $1.size % 2 == 1 end - def foo(format) - pos = 0 - inspects = [] - format.scan(/%[#0\-+ ]?(\*(?=[^0-9])|\*[1-9][0-9]*\$|[1-9][0-9]*(?=[^0-9]))?(\.(\*(?=[^0-9])|\*[1-9][0-9]*\$|[1-9][0-9]*(?=[^0-9])))?(([1-9][0-9]*\$)*)([diouxXeEfgGcsb%])/) {|f, p, pp, pos, new_pos, c| - puts [f, p, pp, pos, new_pos, c].join("!") - pos = new_pos if new_pos - if c == "I" - inspects.push pos.to_i - (f||"")+(p||"")+(pp||"")+(pos||"")+"s" - else - $& - end - } - end - + # Calls #print on each element in the given +objs+, followed by a newline + # character. def puts(*objs) for obj in objs - print(*obj) - print "\n" + print(*obj) + print "\n" end end + # Prints the given +objs+ calling Object#inspect on each. + # + # See #puts for more detail. def pp(*objs) puts(*objs.collect{|obj| obj.inspect}) end + # Prints the given +objs+ calling Object#inspect on each and appending the + # given +prefix+. + # + # See #puts for more detail. def ppx(prefix, *objs) puts(*objs.collect{|obj| prefix+obj.inspect}) end end - class StdioOutputMethod<OutputMethod + # A standard output printer + class StdioOutputMethod < OutputMethod + # Prints the given +opts+ to standard output, see IO#print for more + # information. def print(*opts) STDOUT.print(*opts) end diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index ab584d5253..29862f5507 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -1,82 +1,114 @@ +# frozen_string_literal: false # -# irb/ruby-lex.rb - ruby lexcal analizer -# $Release Version: 0.9.5$ +# irb/ruby-lex.rb - ruby lexcal analyzer +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # -require "e2mmap" -require "irb/slex" -require "irb/ruby-token" +require "ripper" +require "jruby" if RUBY_ENGINE == "jruby" +# :stopdoc: class RubyLex - @RCS_ID='-$Id$-' - - extend Exception2MessageMapper - def_exception(:AlreadyDefinedToken, "Already defined token(%s)") - def_exception(:TkReading2TokenNoKey, "key nothing(key='%s')") - def_exception(:TkSymbol2TokenNoKey, "key nothing(key='%s')") - def_exception(:TkReading2TokenDuplicateError, - "key duplicate(token_n='%s', key='%s')") - def_exception(:SyntaxError, "%s") - def_exception(:TerminateLineInput, "Terminate Line Input") - - include RubyToken - - class << self - attr_accessor :debug_level - def debug? - @debug_level > 0 + class TerminateLineInput < StandardError + def initialize + super("Terminate Line Input") end end - @debug_level = 0 def initialize - lex_init - set_input(STDIN) - - @seek = 0 @exp_line_no = @line_no = 1 - @base_char_no = 0 - @char_no = 0 - @rests = [] - @readed = [] - @here_readed = [] - @indent = 0 - @indent_stack = [] - @lex_state = EXPR_BEG - @space_seen = false - @here_header = false - @continue = false @line = "" - - @skip_space = false - @readed_auto_clean_up = false - @exception_on_syntax_error = true - @prompt = nil end - attr_accessor :skip_space - attr_accessor :readed_auto_clean_up - attr_accessor :exception_on_syntax_error - - attr_reader :seek - attr_reader :char_no - attr_reader :line_no - attr_reader :indent + 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, &block) + 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 + 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 + end + end + if p.respond_to?(:call) @input = p elsif block_given? @@ -86,120 +118,115 @@ class RubyLex end end - def get_readed - if idx = @readed.reverse.index("\n") - @base_char_no = idx - else - @base_char_no += @readed.size - end - - readed = @readed.join("") - @readed = [] - readed - end - - def getc - while @rests.empty? -# return nil unless buf_input - @rests.push nil unless buf_input - end - c = @rests.shift - if @here_header - @here_readed.push c - else - @readed.push c - end - @seek += 1 - if c == "\n" - @line_no += 1 - @char_no = 0 + def set_prompt(p = nil, &block) + p = block if block_given? + if p.respond_to?(:call) + @prompt = p else - @char_no += 1 - end - c - end - - def gets - l = "" - while c = getc - l.concat(c) - break if c == "\n" + @prompt = Proc.new{print p} end - return nil if l == "" and c.nil? - l end - def eof? - @io.eof? - end - - def getc_of_rests - if @rests.empty? - nil - else - getc - end - end + ERROR_TOKENS = [ + :on_parse_error, + :compile_error, + :on_assign_error, + :on_alias_error, + :on_class_name_error, + :on_param_error + ] - def ungetc(c = nil) - if @here_readed.empty? - c2 = @readed.pop - else - c2 = @here_readed.pop - end - c = c2 unless c - @rests.unshift c #c = - @seek -= 1 - if c == "\n" - @line_no -= 1 - if idx = @readed.reverse.index("\n") - @char_no = @readed.size - idx + 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}" + line_no = 0 else - @char_no = @base_char_no + @readed.size + 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+ + 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 + else + pos_to_index[t.pos] = tokens.size + tokens << t + end + end + else + tokens = lexer.parse.reject { |it| it.pos.first == 0 } end - else - @char_no -= 1 - end - end - - def peek_equal?(str) - chrs = str.split(//) - until @rests.size >= chrs.size - return false unless buf_input end - @rests[0, chrs.size] == chrs + tokens + ensure + $VERBOSE = verbose end - def peek_match?(regexp) - while @rests.empty? - return false unless buf_input - end - regexp =~ @rests.join("") + 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 peek(i = 0) - while @rests.size <= i - return nil unless buf_input + 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 + end end - @rests[i] end - def buf_input - prompt - line = @input.call - return nil unless line - @rests.concat line.split(//) - true - end - private :buf_input - - def set_prompt(p = nil, &block) - p = block if block_given? - if p.respond_to?(:call) - @prompt = p - else - @prompt = Proc.new{print p} - 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 @@ -210,940 +237,625 @@ class RubyLex def initialize_input @ltype = nil - @quoted = nil @indent = 0 - @indent_stack = [] - @lex_state = EXPR_BEG - @space_seen = false - @here_header = false - @continue = false - prompt - @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 - @continue = false - prompt - unless l = lex - throw :TERM_INPUT if @line == '' - else - #p l - @line.concat l - if @ltype or @continue or @indent > 0 - next - end - end - if @line != "\n" - yield @line, @exp_line_no - end - break unless l - @line = '' - @exp_line_no = @line_no - - @indent = 0 - @indent_stack = [] - prompt - rescue TerminateLineInput - initialize_input - prompt - get_readed - end + 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 end end end def lex - until (((tk = token).kind_of?(TkNL) || tk.kind_of?(TkEND_OF_SCRIPT)) && - !@continue or - tk.nil?) - #p tk - #p @lex_state - #p self - end - line = get_readed - # print self.inspect - if line == "" and tk.kind_of?(TkEND_OF_SCRIPT) || tk.nil? - nil - else - line - end + 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 token - # require "tracer" - # Tracer.on - @prev_seek = @seek - @prev_line_no = @line_no - @prev_char_no = @char_no - begin - begin - tk = @OP.match(self) - @space_seen = tk.kind_of?(TkSPACE) - rescue SyntaxError - raise if @exception_on_syntax_error - tk = TkError.new(@seek, @line_no, @char_no) - end - end while @skip_space and tk.kind_of?(TkSPACE) - if @readed_auto_clean_up - get_readed - end - # Tracer.off - tk + 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 - - ENINDENT_CLAUSE = [ - "case", "class", "def", "do", "for", "if", - "module", "unless", "until", "while", "begin" #, "when" - ] - DEINDENT_CLAUSE = ["end" #, "when" - ] - - PERCENT_LTYPE = { - "q" => "\'", - "Q" => "\"", - "x" => "\`", - "r" => "/", - "w" => "]", - "W" => "]", - "s" => ":" - } - - PERCENT_PAREN = { - "{" => "}", - "[" => "]", - "<" => ">", - "(" => ")" - } - - Ltype2Token = { - "\'" => TkSTRING, - "\"" => TkSTRING, - "\`" => TkXSTRING, - "/" => TkREGEXP, - "]" => TkDSTRING, - ":" => TkSYMBOL - } - DLtype2Token = { - "\"" => TkDSTRING, - "\`" => TkDXSTRING, - "/" => TkDREGEXP, - } - - def lex_init() - @OP = IRB::SLex.new - @OP.def_rules("\0", "\004", "\032") do |op, io| - Token(TkEND_OF_SCRIPT) - end - - @OP.def_rules(" ", "\t", "\f", "\r", "\13") do |op, io| - @space_seen = true - while getc =~ /[ \t\f\r\13]/; end - ungetc - Token(TkSPACE) - end - - @OP.def_rule("#") do |op, io| - identify_comment - end - - @OP.def_rule("=begin", - proc{|op, io| @prev_char_no == 0 && peek(0) =~ /\s/}) do - |op, io| - @ltype = "=" - until getc == "\n"; end - until peek_equal?("=end") && peek(4) =~ /\s/ - until getc == "\n"; end - end - gets - @ltype = nil - Token(TkRD_COMMENT) - end - @OP.def_rule("\n") do |op, io| - print "\\n\n" if RubyLex.debug? - case @lex_state - when EXPR_BEG, EXPR_FNAME, EXPR_DOT - @continue = true + def check_code_block(code, tokens = @tokens) + return true if tokens.empty? + if tokens.last.event == :on_heredoc_beg + return true + end + + 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 - @continue = false - @lex_state = EXPR_BEG - until (@indent_stack.empty? || - [TkLPAREN, TkLBRACK, TkLBRACE, - TkfLPAREN, TkfLBRACK, TkfLBRACE].include?(@indent_stack.last)) - @indent_stack.pop - end - end - @here_header = false - @here_readed = [] - Token(TkNL) - end - - @OP.def_rules("*", "**", - "=", "==", "===", - "=~", "<=>", - "<", "<=", - ">", ">=", ">>") do - |op, io| - case @lex_state - when EXPR_FNAME, EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_BEG - end - Token(op) - end - - @OP.def_rules("!", "!=", "!~") do - |op, io| - @lex_state = EXPR_BEG - Token(op) - end - - @OP.def_rules("<<") do - |op, io| - tk = nil - if @lex_state != EXPR_END && @lex_state != EXPR_CLASS && - (@lex_state != EXPR_ARG || @space_seen) - c = peek(0) - if /\S/ =~ c && (/["'`]/ =~ c || /[\w_]/ =~ c || c == "-") - tk = identify_here_document - end - end - unless tk - tk = Token(op) - case @lex_state - when EXPR_FNAME, EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_BEG - end - end - tk - end - - @OP.def_rules("'", '"') do - |op, io| - identify_string(op) - end - - @OP.def_rules("`") do - |op, io| - if @lex_state == EXPR_FNAME - @lex_state = EXPR_END - Token(op) - else - identify_string(op) - end - end - - @OP.def_rules('?') do - |op, io| - if @lex_state == EXPR_END - @lex_state = EXPR_BEG - Token(TkQUESTION) - else - ch = getc - if @lex_state == EXPR_ARG && ch =~ /\s/ - ungetc - @lex_state = EXPR_BEG; - Token(TkQUESTION) - else - if (ch == '\\') - read_escape - end - @lex_state = EXPR_END - Token(TkINTEGER) - end - end - end - - @OP.def_rules("&", "&&", "|", "||") do - |op, io| - @lex_state = EXPR_BEG - Token(op) - end - - @OP.def_rules("+=", "-=", "*=", "**=", - "&=", "|=", "^=", "<<=", ">>=", "||=", "&&=") do - |op, io| - @lex_state = EXPR_BEG - op =~ /^(.*)=$/ - Token(TkOPASGN, $1) - end - - @OP.def_rule("+@", proc{|op, io| @lex_state == EXPR_FNAME}) do - |op, io| - @lex_state = EXPR_ARG - Token(op) - end - - @OP.def_rule("-@", proc{|op, io| @lex_state == EXPR_FNAME}) do - |op, io| - @lex_state = EXPR_ARG - Token(op) - end - - @OP.def_rules("+", "-") do - |op, io| - catch(:RET) do - if @lex_state == EXPR_ARG - if @space_seen and peek(0) =~ /[0-9]/ - throw :RET, identify_number - else - @lex_state = EXPR_BEG - end - elsif @lex_state != EXPR_END and peek(0) =~ /[0-9]/ - throw :RET, identify_number - else - @lex_state = EXPR_BEG - end - Token(op) + 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 + ensure + $VERBOSE = verbose end - @OP.def_rule(".") do - |op, io| - @lex_state = EXPR_BEG - if peek(0) =~ /[0-9]/ - ungetc - identify_number - else - # for "obj.if" etc. - @lex_state = EXPR_DOT - Token(TkDOT) + 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 end end - @OP.def_rules("..", "...") do - |op, io| - @lex_state = EXPR_BEG - Token(op) - end - - lex_int2 + false end - - def lex_int2 - @OP.def_rules("]", "}", ")") do - |op, io| - @lex_state = EXPR_END - @indent -= 1 - @indent_stack.pop - Token(op) - end - - @OP.def_rule(":") do - |op, io| - if @lex_state == EXPR_END || peek(0) =~ /\s/ - @lex_state = EXPR_BEG - Token(TkCOLON) - else - @lex_state = EXPR_FNAME; - Token(TkSYMBEG) - end - end - - @OP.def_rule("::") do - |op, io| -# p @lex_state.id2name, @space_seen - if @lex_state == EXPR_BEG or @lex_state == EXPR_ARG && @space_seen - @lex_state = EXPR_BEG - Token(TkCOLON3) - else - @lex_state = EXPR_DOT - Token(TkCOLON2) - end - end - - @OP.def_rule("/") do - |op, io| - if @lex_state == EXPR_BEG || @lex_state == EXPR_MID - identify_string(op) - elsif peek(0) == '=' - getc - @lex_state = EXPR_BEG - Token(TkOPASGN, "/") #/) - elsif @lex_state == EXPR_ARG and @space_seen and peek(0) !~ /\s/ - identify_string(op) - else - @lex_state = EXPR_BEG - Token("/") #/) - end - end - - @OP.def_rules("^") do - |op, io| - @lex_state = EXPR_BEG - Token("^") - end - - # @OP.def_rules("^=") do - # @lex_state = EXPR_BEG - # Token(OP_ASGN, :^) - # end - - @OP.def_rules(",") do - |op, io| - @lex_state = EXPR_BEG - Token(op) - end - - @OP.def_rules(";") do - |op, io| - @lex_state = EXPR_BEG - until (@indent_stack.empty? || - [TkLPAREN, TkLBRACK, TkLBRACE, - TkfLPAREN, TkfLBRACK, TkfLBRACE].include?(@indent_stack.last)) - @indent_stack.pop - end - Token(op) - end - - @OP.def_rule("~") do - |op, io| - @lex_state = EXPR_BEG - Token("~") - end - @OP.def_rule("~@", proc{|op, io| @lex_state == EXPR_FNAME}) do - |op, io| - @lex_state = EXPR_BEG - Token("~") - end - - @OP.def_rule("(") do - |op, io| - @indent += 1 - if @lex_state == EXPR_BEG || @lex_state == EXPR_MID - @lex_state = EXPR_BEG - tk_c = TkfLPAREN + 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 - @lex_state = EXPR_BEG - tk_c = TkLPAREN - end - @indent_stack.push tk_c - tk = Token(tk_c) - end - - @OP.def_rule("[]", proc{|op, io| @lex_state == EXPR_FNAME}) do - |op, io| - @lex_state = EXPR_ARG - Token("[]") - end - - @OP.def_rule("[]=", proc{|op, io| @lex_state == EXPR_FNAME}) do - |op, io| - @lex_state = EXPR_ARG - Token("[]=") - end + 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 + indent -= 1 + 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 + end + end + # percent literals are not indented + } + indent + end - @OP.def_rule("[") do - |op, io| - @indent += 1 - if @lex_state == EXPR_FNAME - tk_c = TkfLBRACK - else - if @lex_state == EXPR_BEG || @lex_state == EXPR_MID - tk_c = TkLBRACK - elsif @lex_state == EXPR_ARG && @space_seen - tk_c = TkLBRACK - else - tk_c = TkfLBRACK - end - @lex_state = EXPR_BEG - end - @indent_stack.push tk_c - Token(tk_c) - 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 + end + end + end + false + end - @OP.def_rule("{") do - |op, io| - @indent += 1 - if @lex_state != EXPR_END && @lex_state != EXPR_ARG - tk_c = TkLBRACE - else - tk_c = TkfLBRACE - end - @lex_state = EXPR_BEG - @indent_stack.push tk_c - Token(tk_c) - 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 + end + syntax_of_do + end - @OP.def_rule('\\') do - |op, io| - if getc == "\n" - @space_seen = true - @continue = true - Token(TkSPACE) - else - ungetc - Token("\\") - end - 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 + end + break if first_in_fomula + end + syntax_of_in + end - @OP.def_rule('%') do - |op, io| - if @lex_state == EXPR_BEG || @lex_state == EXPR_MID - identify_quotation - elsif peek(0) == '=' - getc - Token(TkOPASGN, :%) - elsif @lex_state == EXPR_ARG and @space_seen and peek(0) !~ /\s/ - identify_quotation + 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 else - @lex_state = EXPR_BEG - Token("%") #)) - end - end - - @OP.def_rule('$') do - |op, io| - identify_gvar - end + 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 + 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 + + 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 - @OP.def_rule('@') do - |op, io| - if peek(0) =~ /[\w_@]/ - ungetc - identify_identifier + 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 + + @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 else - Token("@") - end - end - - # @OP.def_rule("def", proc{|op, io| /\s/ =~ io.peek(0)}) do - # |op, io| - # @indent += 1 - # @lex_state = EXPR_FNAME - # # @lex_state = EXPR_END - # # until @rests[0] == "\n" or @rests[0] == ";" - # # rests.shift - # # end - # end - - @OP.def_rule("") do - |op, io| - printf "MATCH: start %s: %s\n", op, io.inspect if RubyLex.debug? - if peek(0) =~ /[0-9]/ - t = identify_number - elsif peek(0) =~ /[\w_]/ - t = identify_identifier - end - printf "MATCH: end %s: %s\n", op, io.inspect if RubyLex.debug? - t - end - - p @OP if RubyLex.debug? - end - - def identify_gvar - @lex_state = EXPR_END - - case ch = getc - when /[~_*$?!@\/\\;,=:<>".]/ #" - Token(TkGVAR, "$" + ch) - when "-" - Token(TkGVAR, "$-" + getc) - when "&", "`", "'", "+" - Token(TkBACK_REF, "$"+ch) - when /[1-9]/ - while getc =~ /[0-9]/; end - ungetc - Token(TkNTH_REF) - when /\w/ - ungetc - ungetc - identify_identifier - else - ungetc - Token("$") - end + 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 + 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 + 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 + else + spaces_of_nest.pop + corresponding_token_depth = nil + 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 + else + spaces_of_nest.pop + corresponding_token_depth = nil + end + end + end + is_first_spaces_of_line = false + is_first_printable_of_line = false + end + corresponding_token_depth end - - def identify_identifier - token = "" - if peek(0) =~ /[$@]/ - token.concat(c = getc) - if c == "@" and peek(0) == "@" - token.concat getc - end - end - - while (ch = getc) =~ /\w|_/ - print ":", ch, ":" if RubyLex.debug? - token.concat ch - end - ungetc - - if (ch == "!" || ch == "?") && token[0,1] =~ /\w/ && peek(0) != "=" - token.concat getc - end - - # almost fix token - - case token - when /^\$/ - return Token(TkGVAR, token) - when /^\@\@/ - @lex_state = EXPR_END - # p Token(TkCVAR, token) - return Token(TkCVAR, token) - when /^\@/ - @lex_state = EXPR_END - return Token(TkIVAR, token) - end - - if @lex_state != EXPR_DOT - print token, "\n" if RubyLex.debug? - - token_c, *trans = TkReading2Token[token] - if token_c - # reserved word? - - if (@lex_state != EXPR_BEG && - @lex_state != EXPR_FNAME && - trans[1]) - # modifiers - token_c = TkSymbol2Token[trans[1]] - @lex_state = trans[0] - else - if @lex_state != EXPR_FNAME - if ENINDENT_CLAUSE.include?(token) - # check for ``class = val'' etc. - valid = true - case token - when "class" - valid = false unless peek_match?(/^\s*(<<|\w|::)/) - when "def" - valid = false if peek_match?(/^\s*(([+-\/*&\|^]|<<|>>|\|\||\&\&)=|\&\&|\|\|)/) - when "do" - valid = false if peek_match?(/^\s*([+-\/*]?=|\*|<|>|\&)/) - when *ENINDENT_CLAUSE - valid = false if peek_match?(/^\s*([+-\/*]?=|\*|<|>|\&|\|)/) - else - # no nothing - end - if valid - if token == "do" - if ![TkFOR, TkWHILE, TkUNTIL].include?(@indent_stack.last) - @indent += 1 - @indent_stack.push token_c - end - else - @indent += 1 - @indent_stack.push token_c - end -# p @indent_stack - end - - elsif DEINDENT_CLAUSE.include?(token) - @indent -= 1 - @indent_stack.pop - end - @lex_state = trans[0] - else - @lex_state = EXPR_END - end - end - return Token(token_c, token) - end - end - - if @lex_state == EXPR_FNAME - @lex_state = EXPR_END - if peek(0) == '=' - token.concat getc - end - elsif @lex_state == EXPR_BEG || @lex_state == EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_END - end - if token[0, 1] =~ /[A-Z]/ - return Token(TkCONSTANT, token) - elsif token[token.size - 1, 1] =~ /[!?]/ - return Token(TkFID, token) - else - return Token(TkIDENTIFIER, token) - 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 - def identify_here_document - ch = getc -# if lt = PERCENT_LTYPE[ch] - if ch == "-" - ch = getc - indent = true - end - if /['"`]/ =~ ch - lt = ch - quoted = "" - while (c = getc) && c != lt - quoted.concat c + 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 ?" end else - lt = '"' - quoted = ch.dup - while (c = getc) && c =~ /\w/ - quoted.concat c - end - ungetc - end - - ltback, @ltype = @ltype, lt - reserve = [] - while ch = getc - reserve.push ch - if ch == "\\" - reserve.push ch = getc - elsif ch == "\n" - break - end - end - - @here_header = false - while l = gets - l = l.sub(/(:?\r)?\n\z/, '') - if (indent ? l.strip : l) == quoted - break - end - end - - @here_header = true - @here_readed.concat reserve - while ch = reserve.pop - ungetc ch - end - - @ltype = ltback - @lex_state = EXPR_END - Token(Ltype2Token[lt]) - end - - def identify_quotation - ch = getc - if lt = PERCENT_LTYPE[ch] - ch = getc - elsif ch =~ /\W/ - lt = "\"" - else - RubyLex.fail SyntaxError, "unknown type of %string" + nil end -# if ch !~ /\W/ -# ungetc -# next -# end - #@ltype = lt - @quoted = ch unless @quoted = PERCENT_PAREN[ch] - identify_string(lt, @quoted) end - def identify_number - @lex_state = EXPR_END - - if peek(0) == "0" && peek(1) !~ /[.eE]/ - getc - case peek(0) - when /[xX]/ - ch = getc - match = /[0-9a-fA-F_]/ - when /[bB]/ - ch = getc - match = /[01_]/ - when /[oO]/ - ch = getc - match = /[0-7_]/ - when /[dD]/ - ch = getc - match = /[0-9_]/ - when /[0-7]/ - match = /[0-7_]/ - when /[89]/ - RubyLex.fail SyntaxError, "Illegal octal digit" - else - return Token(TkINTEGER) - end - - len0 = true - non_digit = false - while ch = getc - if match =~ ch - if ch == "_" - if non_digit - RubyLex.fail SyntaxError, "trailing `#{ch}' in number" - else - non_digit = ch - end - else - non_digit = false - len0 = false - end - else - ungetc - if len0 - RubyLex.fail SyntaxError, "numeric literal without digits" - end - if non_digit - RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number" - end - break - end - end - return Token(TkINTEGER) - end - - type = TkINTEGER - allow_point = true - allow_e = true - non_digit = false - while ch = getc - case ch - when /[0-9]/ - non_digit = false - when "_" - non_digit = ch - when allow_point && "." - if non_digit - RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number" - end - type = TkFLOAT - if peek(0) !~ /[0-9]/ - type = TkINTEGER - ungetc - break - end - allow_point = false - when allow_e && "e", allow_e && "E" - if non_digit - RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number" - end - type = TkFLOAT - if peek(0) =~ /[+-]/ - getc - end - allow_e = false - allow_point = false - non_digit = ch + 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 + end + elsif t.tok.include?("\n") + past_first_newline = true + false else - if non_digit - RubyLex.fail SyntaxError, "trailing `#{non_digit}' in number" - end - ungetc - break + false end end - Token(type) - end - - def identify_string(ltype, quoted = ltype) - @ltype = ltype - @quoted = quoted - subtype = nil - begin - nest = 0 - while ch = getc - if @quoted == ch and nest == 0 - break - elsif @ltype != "'" && @ltype != "]" && @ltype != ":" and ch == "#" - subtype = true - elsif ch == '\\' #' - read_escape - end - if PERCENT_PAREN.values.include?(@quoted) - if PERCENT_PAREN[ch] == @quoted - nest += 1 - elsif ch == @quoted - nest -= 1 - end - end - end - if @ltype == "/" - if peek(0) =~ /i|m|x|o|e|s|u|n/ - getc - 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 subtype - Token(DLtype2Token[ltype]) + + if first_token.nil? + return false + elsif first_token && first_token.state == Ripper::EXPR_DOT + return false else - Token(Ltype2Token[ltype]) - end - ensure - @ltype = nil - @quoted = nil - @lex_state = EXPR_END - end + 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 + else + return last_line_tokens.map(&:tok).join('') + end + end + end + false end - - def identify_comment - @ltype = "#" - while ch = getc -# if ch == "\\" #" -# read_escape -# end - if ch == "\n" - @ltype = nil - ungetc - break - end - end - return Token(TkCOMMENT) - end - - def read_escape - case ch = getc - when "\n", "\r", "\f" - when "\\", "n", "t", "r", "f", "v", "a", "e", "b", "s" #" - when /[0-7]/ - ungetc ch - 3.times do - case ch = getc - when /[0-7]/ - when nil - break - else - ungetc - break - end - end - - when "x" - 2.times do - case ch = getc - when /[0-9a-fA-F]/ - when nil - break - else - ungetc - break - end - end + private - when "M" - if (ch = getc) != '-' - ungetc - else - if (ch = getc) == "\\" #" - read_escape - end - end + 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 - when "C", "c" #, "^" - if ch == "C" and (ch = getc) != "-" - ungetc - elsif (ch = getc) == "\\" #" - read_escape + 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 end - else - # other characters end + false end end +# :startdoc: diff --git a/lib/irb/ruby-token.rb b/lib/irb/ruby-token.rb deleted file mode 100644 index 525d4df14c..0000000000 --- a/lib/irb/ruby-token.rb +++ /dev/null @@ -1,273 +0,0 @@ -# -# irb/ruby-token.rb - ruby tokens -# $Release Version: 0.9.5$ -# $Revision$ -# $Date$ -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# -# -- -# -# -# -module RubyToken - EXPR_BEG = :EXPR_BEG - EXPR_MID = :EXPR_MID - EXPR_END = :EXPR_END - EXPR_ARG = :EXPR_ARG - EXPR_FNAME = :EXPR_FNAME - EXPR_DOT = :EXPR_DOT - EXPR_CLASS = :EXPR_CLASS - - # for ruby 1.4X - if !defined?(Symbol) - Symbol = Integer - end - - class Token - def initialize(seek, line_no, char_no) - @seek = seek - @line_no = line_no - @char_no = char_no - end - attr :seek - attr :line_no - attr :char_no - end - - class TkNode < Token - def initialize(seek, line_no, char_no) - super - end - attr :node - end - - class TkId < Token - def initialize(seek, line_no, char_no, name) - super(seek, line_no, char_no) - @name = name - end - attr :name - end - - class TkVal < Token - def initialize(seek, line_no, char_no, value = nil) - super(seek, line_no, char_no) - @value = value - end - attr :value - end - - class TkOp < Token - attr :name, true - end - - class TkOPASGN < TkOp - def initialize(seek, line_no, char_no, op) - super(seek, line_no, char_no) - op = TkReading2Token[op][0] unless op.kind_of?(Symbol) - @op = op - end - attr :op - end - - class TkUnknownChar < Token - def initialize(seek, line_no, char_no, id) - super(seek, line_no, char_no) - @name = name - end - attr :name - end - - class TkError < Token - end - - def Token(token, value = nil) - case token - when String - if (tk = TkReading2Token[token]).nil? - IRB.fail TkReading2TokenNoKey, token - end - tk = Token(tk[0], value) - if tk.kind_of?(TkOp) - tk.name = token - end - return tk - when Symbol - if (tk = TkSymbol2Token[token]).nil? - IRB.fail TkSymbol2TokenNoKey, token - end - return Token(tk[0], value) - else - if (token.ancestors & [TkId, TkVal, TkOPASGN, TkUnknownChar]).empty? - token.new(@prev_seek, @prev_line_no, @prev_char_no) - else - token.new(@prev_seek, @prev_line_no, @prev_char_no, value) - end - end - end - - TokenDefinitions = [ - [:TkCLASS, TkId, "class", EXPR_CLASS], - [:TkMODULE, TkId, "module", EXPR_BEG], - [:TkDEF, TkId, "def", EXPR_FNAME], - [:TkUNDEF, TkId, "undef", EXPR_FNAME], - [:TkBEGIN, TkId, "begin", EXPR_BEG], - [:TkRESCUE, TkId, "rescue", EXPR_MID], - [:TkENSURE, TkId, "ensure", EXPR_BEG], - [:TkEND, TkId, "end", EXPR_END], - [:TkIF, TkId, "if", EXPR_BEG, :TkIF_MOD], - [:TkUNLESS, TkId, "unless", EXPR_BEG, :TkUNLESS_MOD], - [:TkTHEN, TkId, "then", EXPR_BEG], - [:TkELSIF, TkId, "elsif", EXPR_BEG], - [:TkELSE, TkId, "else", EXPR_BEG], - [:TkCASE, TkId, "case", EXPR_BEG], - [:TkWHEN, TkId, "when", EXPR_BEG], - [:TkWHILE, TkId, "while", EXPR_BEG, :TkWHILE_MOD], - [:TkUNTIL, TkId, "until", EXPR_BEG, :TkUNTIL_MOD], - [:TkFOR, TkId, "for", EXPR_BEG], - [:TkBREAK, TkId, "break", EXPR_END], - [:TkNEXT, TkId, "next", EXPR_END], - [:TkREDO, TkId, "redo", EXPR_END], - [:TkRETRY, TkId, "retry", EXPR_END], - [:TkIN, TkId, "in", EXPR_BEG], - [:TkDO, TkId, "do", EXPR_BEG], - [:TkRETURN, TkId, "return", EXPR_MID], - [:TkYIELD, TkId, "yield", EXPR_END], - [:TkSUPER, TkId, "super", EXPR_END], - [:TkSELF, TkId, "self", EXPR_END], - [:TkNIL, TkId, "nil", EXPR_END], - [:TkTRUE, TkId, "true", EXPR_END], - [:TkFALSE, TkId, "false", EXPR_END], - [:TkAND, TkId, "and", EXPR_BEG], - [:TkOR, TkId, "or", EXPR_BEG], - [:TkNOT, TkId, "not", EXPR_BEG], - [:TkIF_MOD, TkId], - [:TkUNLESS_MOD, TkId], - [:TkWHILE_MOD, TkId], - [:TkUNTIL_MOD, TkId], - [:TkALIAS, TkId, "alias", EXPR_FNAME], - [:TkDEFINED, TkId, "defined?", EXPR_END], - [:TklBEGIN, TkId, "BEGIN", EXPR_END], - [:TklEND, TkId, "END", EXPR_END], - [:Tk__LINE__, TkId, "__LINE__", EXPR_END], - [:Tk__FILE__, TkId, "__FILE__", EXPR_END], - - [:TkIDENTIFIER, TkId], - [:TkFID, TkId], - [:TkGVAR, TkId], - [:TkCVAR, TkId], - [:TkIVAR, TkId], - [:TkCONSTANT, TkId], - - [:TkINTEGER, TkVal], - [:TkFLOAT, TkVal], - [:TkSTRING, TkVal], - [:TkXSTRING, TkVal], - [:TkREGEXP, TkVal], - [:TkSYMBOL, TkVal], - - [:TkDSTRING, TkNode], - [:TkDXSTRING, TkNode], - [:TkDREGEXP, TkNode], - [:TkNTH_REF, TkNode], - [:TkBACK_REF, TkNode], - - [:TkUPLUS, TkOp, "+@"], - [:TkUMINUS, TkOp, "-@"], - [:TkPOW, TkOp, "**"], - [:TkCMP, TkOp, "<=>"], - [:TkEQ, TkOp, "=="], - [:TkEQQ, TkOp, "==="], - [:TkNEQ, TkOp, "!="], - [:TkGEQ, TkOp, ">="], - [:TkLEQ, TkOp, "<="], - [:TkANDOP, TkOp, "&&"], - [:TkOROP, TkOp, "||"], - [:TkMATCH, TkOp, "=~"], - [:TkNMATCH, TkOp, "!~"], - [:TkDOT2, TkOp, ".."], - [:TkDOT3, TkOp, "..."], - [:TkAREF, TkOp, "[]"], - [:TkASET, TkOp, "[]="], - [:TkLSHFT, TkOp, "<<"], - [:TkRSHFT, TkOp, ">>"], - [:TkCOLON2, TkOp], - [:TkCOLON3, TkOp], -# [:OPASGN, TkOp], # +=, -= etc. # - [:TkASSOC, TkOp, "=>"], - [:TkQUESTION, TkOp, "?"], #? - [:TkCOLON, TkOp, ":"], #: - - [:TkfLPAREN], # func( # - [:TkfLBRACK], # func[ # - [:TkfLBRACE], # func{ # - [:TkSTAR], # *arg - [:TkAMPER], # &arg # - [:TkSYMBEG], # :SYMBOL - - [:TkGT, TkOp, ">"], - [:TkLT, TkOp, "<"], - [:TkPLUS, TkOp, "+"], - [:TkMINUS, TkOp, "-"], - [:TkMULT, TkOp, "*"], - [:TkDIV, TkOp, "/"], - [:TkMOD, TkOp, "%"], - [:TkBITOR, TkOp, "|"], - [:TkBITXOR, TkOp, "^"], - [:TkBITAND, TkOp, "&"], - [:TkBITNOT, TkOp, "~"], - [:TkNOTOP, TkOp, "!"], - - [:TkBACKQUOTE, TkOp, "`"], - - [:TkASSIGN, Token, "="], - [:TkDOT, Token, "."], - [:TkLPAREN, Token, "("], #(exp) - [:TkLBRACK, Token, "["], #[arry] - [:TkLBRACE, Token, "{"], #{hash} - [:TkRPAREN, Token, ")"], - [:TkRBRACK, Token, "]"], - [:TkRBRACE, Token, "}"], - [:TkCOMMA, Token, ","], - [:TkSEMICOLON, Token, ";"], - - [:TkCOMMENT], - [:TkRD_COMMENT], - [:TkSPACE], - [:TkNL], - [:TkEND_OF_SCRIPT], - - [:TkBACKSLASH, TkUnknownChar, "\\"], - [:TkAT, TkUnknownChar, "@"], - [:TkDOLLAR, TkUnknownChar, "$"], - ] - - # {reading => token_class} - # {reading => [token_class, *opt]} - TkReading2Token = {} - TkSymbol2Token = {} - - def RubyToken.def_token(token_n, super_token = Token, reading = nil, *opts) - token_n = token_n.id2name if token_n.kind_of?(Symbol) - if RubyToken.const_defined?(token_n) - IRB.fail AlreadyDefinedToken, token_n - end - token_c = eval("class #{token_n} < #{super_token}; end; #{token_n}") - - if reading - if TkReading2Token[reading] - IRB.fail TkReading2TokenDuplicateError, token_n, reading - end - if opts.empty? - TkReading2Token[reading] = [token_c] - else - TkReading2Token[reading] = [token_c].concat(opts) - end - end - TkSymbol2Token[token_n.intern] = token_c - end - - for defs in TokenDefinitions - def_token(*defs) - end -end diff --git a/lib/irb/ruby_logo.aa b/lib/irb/ruby_logo.aa new file mode 100644 index 0000000000..a34a3e2f28 --- /dev/null +++ b/lib/irb/ruby_logo.aa @@ -0,0 +1,37 @@ + + -+smJYYN?mm- + HB"BBYT TQg NggT + 9Q+g Nm,T 8g NJW + YS+ N2NJ"Sg N? + BQg #( gT Nggggk J + 5j NJ NJ NNge + #Q #JJ NgT N( + @j bj mT J + Bj @/d NJ ( + #q #(( NgT #J + 5d #(t mT $d + #q @(@J NJB; + @( 5d ? HHH H HQmgggggggmN qD + 5d #uN 2QdH E O + 5 5JSd Nd NJH @d j + Fd @J4d s NQH #d ( + #( #o6d Nd NgH #d #d + 4 B&Od v NgT #d F + #( 9JGd NH NgUd F + #d #GJQ d NP $ + #J #U+#Q N Q # j + j /W BQ+ BQ d NJ NJ + - NjJH HBIjTQggPJQgW N W k #J + #J b HYWgggN j s Nag d NN b #d + #J 5- D s Ngg N d Nd F + Fd BKH2 #+ s NNgg J Q J ] + F H @ J N y K(d P I + F4 E N? #d y #Q NJ E j + F W Nd q m Bg NxW N(H- + F d b @ m Hd gW vKJ + NJ d K d s Bg aT FDd + b # d N m BQ mV N> + e5 Nd #d NggggggQWH HHHH NJ - + m7 NW H N HSVO1z=?11- + NgTH bB kH WBHWWHBHWmQgg&gggggNNN + NNggggggNN diff --git a/lib/irb/slex.rb b/lib/irb/slex.rb deleted file mode 100644 index a6ea6fb473..0000000000 --- a/lib/irb/slex.rb +++ /dev/null @@ -1,285 +0,0 @@ -# -# irb/slex.rb - symple lex analizer -# $Release Version: 0.9.5$ -# $Revision$ -# $Date$ -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# -# -- -# -# -# - -require "e2mmap" -require "irb/notifier" - -module IRB - class SLex - @RCS_ID='-$Id$-' - - extend Exception2MessageMapper - def_exception :ErrNodeNothing, "node nothing" - def_exception :ErrNodeAlreadyExists, "node already exists" - - DOUT = Notifier::def_notifier("SLex::") - D_WARN = DOUT::def_notifier(1, "Warn: ") - D_DEBUG = DOUT::def_notifier(2, "Debug: ") - D_DETAIL = DOUT::def_notifier(4, "Detail: ") - - DOUT.level = Notifier::D_NOMSG - - def initialize - @head = Node.new("") - end - - def def_rule(token, preproc = nil, postproc = nil, &block) - D_DETAIL.pp token - - postproc = block if block_given? - node = create(token, preproc, postproc) - end - - def def_rules(*tokens, &block) - if block_given? - p = block - end - for token in tokens - def_rule(token, nil, p) - end - end - - def preproc(token, proc) - node = search(token) - node.preproc=proc - end - - #要チェック? - def postproc(token) - node = search(token, proc) - node.postproc=proc - end - - def search(token) - @head.search(token.split(//)) - end - - def create(token, preproc = nil, postproc = nil) - @head.create_subnode(token.split(//), preproc, postproc) - end - - def match(token) - case token - when Array - when String - return match(token.split(//)) - else - return @head.match_io(token) - end - ret = @head.match(token) - D_DETAIL.exec_if{D_DEATIL.printf "match end: %s:%s\n", ret, token.inspect} - ret - end - - def inspect - format("<SLex: @head = %s>", @head.inspect) - end - - #---------------------------------------------------------------------- - # - # class Node - - # - #---------------------------------------------------------------------- - class Node - # if postproc is nil, this node is an abstract node. - # if postproc is non-nil, this node is a real node. - def initialize(preproc = nil, postproc = nil) - @Tree = {} - @preproc = preproc - @postproc = postproc - end - - attr_accessor :preproc - attr_accessor :postproc - - def search(chrs, opt = nil) - return self if chrs.empty? - ch = chrs.shift - if node = @Tree[ch] - node.search(chrs, opt) - else - if opt - chrs.unshift ch - self.create_subnode(chrs) - else - SLex.fail ErrNodeNothing - end - end - end - - def create_subnode(chrs, preproc = nil, postproc = nil) - if chrs.empty? - if @postproc - D_DETAIL.pp node - SLex.fail ErrNodeAlreadyExists - else - D_DEBUG.puts "change abstract node to real node." - @preproc = preproc - @postproc = postproc - end - return self - end - - ch = chrs.shift - if node = @Tree[ch] - if chrs.empty? - if node.postproc - DebugLogger.pp node - DebugLogger.pp self - DebugLogger.pp ch - DebugLogger.pp chrs - SLex.fail ErrNodeAlreadyExists - else - D_WARN.puts "change abstract node to real node" - node.preproc = preproc - node.postproc = postproc - end - else - node.create_subnode(chrs, preproc, postproc) - end - else - if chrs.empty? - node = Node.new(preproc, postproc) - else - node = Node.new - node.create_subnode(chrs, preproc, postproc) - end - @Tree[ch] = node - end - node - end - - # - # chrs: String - # character array - # io must have getc()/ungetc(); and ungetc() must be - # able to be called arbitrary number of times. - # - def match(chrs, op = "") - D_DETAIL.print "match>: ", chrs, "op:", op, "\n" - if chrs.empty? - if @preproc.nil? || @preproc.call(op, chrs) - DOUT.printf(D_DETAIL, "op1: %s\n", op) - @postproc.call(op, chrs) - else - nil - end - else - ch = chrs.shift - if node = @Tree[ch] - if ret = node.match(chrs, op+ch) - return ret - else - chrs.unshift ch - if @postproc and @preproc.nil? || @preproc.call(op, chrs) - DOUT.printf(D_DETAIL, "op2: %s\n", op.inspect) - ret = @postproc.call(op, chrs) - return ret - else - return nil - end - end - else - chrs.unshift ch - if @postproc and @preproc.nil? || @preproc.call(op, chrs) - DOUT.printf(D_DETAIL, "op3: %s\n", op) - @postproc.call(op, chrs) - return "" - else - return nil - end - end - end - end - - def match_io(io, op = "") - if op == "" - ch = io.getc - if ch == nil - return nil - end - else - ch = io.getc_of_rests - end - if ch.nil? - if @preproc.nil? || @preproc.call(op, io) - D_DETAIL.printf("op1: %s\n", op) - @postproc.call(op, io) - else - nil - end - else - if node = @Tree[ch] - if ret = node.match_io(io, op+ch) - ret - else - io.ungetc ch - if @postproc and @preproc.nil? || @preproc.call(op, io) - DOUT.exec_if{D_DETAIL.printf "op2: %s\n", op.inspect} - @postproc.call(op, io) - else - nil - end - end - else - io.ungetc ch - if @postproc and @preproc.nil? || @preproc.call(op, io) - D_DETAIL.printf("op3: %s\n", op) - @postproc.call(op, io) - else - nil - end - end - end - end - end - end -end - -SLex=IRB::SLex - -if $0 == __FILE__ - # Tracer.on - case $1 - when "1" - tr = SLex.new - print "0: ", tr.inspect, "\n" - tr.def_rule("=") {print "=\n"} - print "1: ", tr.inspect, "\n" - tr.def_rule("==") {print "==\n"} - print "2: ", tr.inspect, "\n" - - print "case 1:\n" - print tr.match("="), "\n" - print "case 2:\n" - print tr.match("=="), "\n" - print "case 3:\n" - print tr.match("=>"), "\n" - - when "2" - tr = SLex.new - print "0: ", tr.inspect, "\n" - tr.def_rule("=") {print "=\n"} - print "1: ", tr.inspect, "\n" - tr.def_rule("==", proc{false}) {print "==\n"} - print "2: ", tr.inspect, "\n" - - print "case 1:\n" - print tr.match("="), "\n" - print "case 2:\n" - print tr.match("=="), "\n" - print "case 3:\n" - print tr.match("=>"), "\n" - end - exit -end - diff --git a/lib/irb/src_encoding.rb b/lib/irb/src_encoding.rb new file mode 100644 index 0000000000..99aea2b43e --- /dev/null +++ b/lib/irb/src_encoding.rb @@ -0,0 +1,7 @@ +# 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/version.rb b/lib/irb/version.rb index 28b079740a..481d14ffd2 100644 --- a/lib/irb/version.rb +++ b/lib/irb/version.rb @@ -1,16 +1,17 @@ +# frozen_string_literal: false # # irb/version.rb - irb version definition file -# $Release Version: 0.9.5$ +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ishitsuka.com) # # -- # -# +# # -module IRB - @RELEASE_VERSION = "0.9.5" - @LAST_UPDATE_DATE = "05/04/13" +module IRB # :nodoc: + VERSION = "1.4.1" + @RELEASE_VERSION = VERSION + @LAST_UPDATE_DATE = "2021-12-25" end diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb index 7d1794cd7b..2c4c40f348 100644 --- a/lib/irb/workspace.rb +++ b/lib/irb/workspace.rb @@ -1,106 +1,186 @@ +# frozen_string_literal: false # -# irb/workspace-binding.rb - -# $Release Version: 0.9.5$ +# irb/workspace-binding.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# # -module IRB +# + +require "delegate" + +IRB::TOPLEVEL_BINDING = binding +module IRB # :nodoc: class WorkSpace - # create new workspace. set self to main if specified, otherwise + # Creates a new workspace. + # + # set self to main if specified, otherwise # inherit main from TOPLEVEL_BINDING. def initialize(*main) if main[0].kind_of?(Binding) - @binding = main.shift + @binding = main.shift elsif IRB.conf[:SINGLE_IRB] - @binding = TOPLEVEL_BINDING + @binding = TOPLEVEL_BINDING else - case IRB.conf[:CONTEXT_MODE] - when 0 # binding in proc on TOPLEVEL_BINDING - @binding = eval("proc{binding}.call", - TOPLEVEL_BINDING, - __FILE__, - __LINE__) - when 1 # binding in loaded file - require "tempfile" - f = Tempfile.open("irb-binding") - f.print <<EOF - $binding = binding + case IRB.conf[:CONTEXT_MODE] + when 0 # binding in proc on TOPLEVEL_BINDING + @binding = eval("proc{binding}.call", + TOPLEVEL_BINDING, + __FILE__, + __LINE__) + when 1 # binding in loaded file + require "tempfile" + f = Tempfile.open("irb-binding") + f.print <<EOF + $binding = binding EOF - f.close - load f.path - @binding = $binding - - when 2 # binding in loaded file(thread use) - unless defined? BINDING_QUEUE - require "thread" - - IRB.const_set("BINDING_QUEUE", SizedQueue.new(1)) - Thread.abort_on_exception = true - Thread.start do - eval "require \"irb/ws-for-case-2\"", TOPLEVEL_BINDING, __FILE__, __LINE__ - end - Thread.pass - end - @binding = BINDING_QUEUE.pop - - when 3 # binging in function on TOPLEVEL_BINDING(default) - @binding = eval("def irb_binding; binding; end; irb_binding", - TOPLEVEL_BINDING, - __FILE__, - __LINE__ - 3) - end + f.close + load f.path + @binding = $binding + + when 2 # binding in loaded file(thread use) + unless defined? BINDING_QUEUE + IRB.const_set(:BINDING_QUEUE, Thread::SizedQueue.new(1)) + Thread.abort_on_exception = true + Thread.start do + eval "require \"irb/ws-for-case-2\"", TOPLEVEL_BINDING, __FILE__, __LINE__ + end + Thread.pass + end + @binding = BINDING_QUEUE.pop + + when 3 # binding in function on TOPLEVEL_BINDING + @binding = eval("self.class.remove_method(:irb_binding) if defined?(irb_binding); private; def irb_binding; binding; end; irb_binding", + TOPLEVEL_BINDING, + __FILE__, + __LINE__ - 3) + when 4 # binding is a copy of TOPLEVEL_BINDING (default) + # Note that this will typically be IRB::TOPLEVEL_BINDING + # This is to avoid RubyGems' local variables (see issue #17623) + @binding = TOPLEVEL_BINDING.dup + end end + if main.empty? - @main = eval("self", @binding) + @main = eval("self", @binding) else - @main = main[0] - IRB.conf[:__MAIN__] = @main - case @main - when Module - @binding = eval("IRB.conf[:__MAIN__].module_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__) - else - begin - @binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__) - rescue TypeError - IRB.fail CantChangeBinding, @main.inspect - end - end + @main = main[0] + end + IRB.conf[:__MAIN__] = @main + + unless main.empty? + case @main + when Module + @binding = eval("IRB.conf[:__MAIN__].module_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__) + else + begin + @binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__) + rescue TypeError + fail CantChangeBinding, @main.inspect + end + end end - eval("_=nil", @binding) + + case @main + when Object + use_delegator = @main.frozen? + else + use_delegator = true + end + + if use_delegator + @main = SimpleDelegator.new(@main) + 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)) + end + @binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, *@binding.source_location) + end + + @binding.local_variable_set(:_, nil) end + # The Binding of this workspace attr_reader :binding + # The top-level workspace of this context, also available as + # <code>IRB.conf[:__MAIN__]</code> attr_reader :main + # Evaluate the given +statements+ within the context of this workspace. def evaluate(context, statements, file = __FILE__, line = __LINE__) eval(statements, @binding, file, line) end - + + def local_variable_set(name, value) + @binding.local_variable_set(name, value) + end + + def local_variable_get(name) + @binding.local_variable_get(name) + end + # error message manipulator def filter_backtrace(bt) + return nil if bt =~ /\/irb\/.*\.rb/ + return nil if bt =~ /\/irb\.rb/ + return nil if bt =~ /tool\/lib\/.*\.rb|runner\.rb/ # for tests in Ruby repository case IRB.conf[:CONTEXT_MODE] - when 0 - return nil if bt =~ /\(irb_local_binding\)/ when 1 - if(bt =~ %r!/tmp/irb-binding! or - bt =~ %r!irb/.*\.rb! or - bt =~ /irb\.rb/) - return nil - end - when 2 - return nil if bt =~ /irb\/.*\.rb/ + return nil if bt =~ %r!/tmp/irb-binding! when 3 - return nil if bt =~ /irb\/.*\.rb/ - bt.sub!(/:\s*in `irb_binding'/){""} + bt = bt.sub(/:\s*in `irb_binding'/, '') end bt end + def code_around_binding + if @binding.respond_to?(:source_location) + file, pos = @binding.source_location + else + file, pos = @binding.eval('[__FILE__, __LINE__]') + end + + if defined?(::SCRIPT_LINES__[file]) && lines = ::SCRIPT_LINES__[file] + code = ::SCRIPT_LINES__[file].join('') + else + begin + code = File.read(file) + rescue SystemCallError + return + end + end + + # NOT using #use_colorize? of IRB.conf[:MAIN_CONTEXT] because this method may be called before IRB::Irb#run + use_colorize = IRB.conf.fetch(:USE_COLORIZE, true) + if use_colorize + lines = Color.colorize_code(code).lines + else + lines = code.lines + end + pos -= 1 + + start_pos = [pos - 5, 0].max + end_pos = [pos + 5, lines.size - 1].min + + if use_colorize + fmt = " %2s #{Color.colorize("%#{end_pos.to_s.length}d", [:BLUE, :BOLD])}: %s" + else + fmt = " %2s %#{end_pos.to_s.length}d: %s" + end + body = (start_pos..end_pos).map do |current_pos| + sprintf(fmt, pos == current_pos ? '=>' : '', current_pos + 1, lines[current_pos]) + end.join("") + "\nFrom: #{file} @ line #{pos + 1} :\n\n#{body}#{Color.clear if use_colorize}\n" + end + def IRB.delete_caller end end diff --git a/lib/irb/ws-for-case-2.rb b/lib/irb/ws-for-case-2.rb index afd49d23e1..eb173fddca 100644 --- a/lib/irb/ws-for-case-2.rb +++ b/lib/irb/ws-for-case-2.rb @@ -1,15 +1,15 @@ +# frozen_string_literal: false # -# irb/ws-for-case-2.rb - -# $Release Version: 0.9.5$ +# irb/ws-for-case-2.rb - +# $Release Version: 0.9.6$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(keiju@ruby-lang.org) # # -- # -# +# # while true - IRB::BINDING_QUEUE.push b = binding + IRB::BINDING_QUEUE.push _ = binding end diff --git a/lib/irb/xmp.rb b/lib/irb/xmp.rb index 4bcc2ca22f..88cbd88525 100644 --- a/lib/irb/xmp.rb +++ b/lib/irb/xmp.rb @@ -1,25 +1,59 @@ +# frozen_string_literal: false # # xmp.rb - irb version of gotoken xmp # $Release Version: 0.9$ # $Revision$ -# $Date$ # by Keiju ISHITSUKA(Nippon Rational Inc.) # # -- # -# +# # -require "irb" -require "irb/frame" +require_relative "../irb" +require_relative "frame" +# An example printer for irb. +# +# It's much like the standard library PrettyPrint, that shows the value of each +# expression as it runs. +# +# In order to use this library, you must first require it: +# +# require 'irb/xmp' +# +# Now, you can take advantage of the Object#xmp convenience method. +# +# xmp <<END +# foo = "bar" +# baz = 42 +# END +# #=> foo = "bar" +# #==>"bar" +# #=> baz = 42 +# #==>42 +# +# You can also create an XMP object, with an optional binding to print +# expressions in the given binding: +# +# ctx = binding +# x = XMP.new ctx +# x.puts +# #=> today = "a good day" +# #==>"a good day" +# ctx.eval 'today # is what?' +# #=> "a good day" class XMP - @RCS_ID='-$Id$-' + # Creates a new XMP object. + # + # 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. def initialize(bind = nil) IRB.init_config(nil) - #IRB.parse_opts - #IRB.load_modules IRB.conf[:PROMPT_MODE] = :XMP @@ -29,55 +63,105 @@ class XMP @irb = IRB::Irb.new(ws, @io) @irb.context.ignore_sigint = false -# IRB.conf[:IRB_RC].call(@irb.context) if IRB.conf[:IRB_RC] IRB.conf[:MAIN_CONTEXT] = @irb.context end + # Evaluates the given +exps+, for example: + # + # require 'irb/xmp' + # x = XMP.new + # + # x.puts '{:a => 1, :b => 2, :c => 3}' + # #=> {:a => 1, :b => 2, :c => 3} + # # ==>{:a=>1, :b=>2, :c=>3} + # x.puts 'foo = "bar"' + # # => foo = "bar" + # # ==>"bar" def puts(exps) @io.puts exps if @irb.context.ignore_sigint begin - trap_proc_b = trap("SIGINT"){@irb.signal_handle} - catch(:IRB_EXIT) do - @irb.eval_input - end + trap_proc_b = trap("SIGINT"){@irb.signal_handle} + catch(:IRB_EXIT) do + @irb.eval_input + end ensure - trap("SIGINT", trap_proc_b) + trap("SIGINT", trap_proc_b) end else catch(:IRB_EXIT) do - @irb.eval_input + @irb.eval_input end end end + # A custom InputMethod class used by XMP for evaluating string io. class StringInputMethod < IRB::InputMethod + # Creates a new StringInputMethod object def initialize super @exps = [] end + # Whether there are any expressions left in this printer. def eof? @exps.empty? end + # Reads the next expression from this printer. + # + # See IO#gets for more information. def gets while l = @exps.shift - next if /^\s+$/ =~ l - l.concat "\n" - print @prompt, l - break + next if /^\s+$/ =~ l + l.concat "\n" + print @prompt, l + break end l end + # Concatenates all expressions in this printer, separated by newlines. + # + # An Encoding::CompatibilityError is raised of the given +exps+'s encoding + # doesn't match the previous expression evaluated. def puts(exps) + if @encoding and exps.encoding != @encoding + enc = Encoding.compatible?(@exps.join("\n"), exps) + if enc.nil? + raise Encoding::CompatibilityError, "Encoding in which the passed expression is encoded is not compatible to the preceding's one" + else + @encoding = enc + end + else + @encoding = exps.encoding + end @exps.concat exps.split(/\n/) end + + # Returns the encoding of last expression printed by #puts. + attr_reader :encoding end end +# A convenience method that's only available when the you require the IRB::XMP standard library. +# +# Creates a new XMP object, using the given expressions as the +exps+ +# parameter, and optional binding as +bind+ or uses the top-level binding. Then +# evaluates the given expressions using the +:XMP+ prompt mode. +# +# For example: +# +# require 'irb/xmp' +# ctx = binding +# xmp 'foo = "bar"', ctx +# #=> foo = "bar" +# #==>"bar" +# ctx.eval 'foo' +# #=> "bar" +# +# See XMP.new for more information. def xmp(exps, bind = nil) bind = IRB::Frame.top(1) unless bind xmp = XMP.new(bind) |
