diff options
30 files changed, 920 insertions, 139 deletions
diff --git a/lib/irb.rb b/lib/irb.rb index 7f99974f28..93c4d25c92 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -60,7 +60,11 @@ require_relative "irb/easter-egg" # -E enc Same as `ruby -E` # -w Same as `ruby -w` # -W[level=2] Same as `ruby -W` -# --inspect Use `inspect' for output (default except for bc mode) +# --context-mode n Set n[0-4] to method to create Binding Object, +# when new workspace was created +# --echo Show result(default) +# --noecho Don't show result +# --inspect Use `inspect' for output # --noinspect Don't use inspect for output # --multiline Use multiline editor module # --nomultiline Don't use multiline editor module @@ -68,19 +72,24 @@ require_relative "irb/easter-egg" # --nosingleline Don't use singleline editor module # --colorize Use colorization # --nocolorize Don't use colorization -# --prompt prompt-mode -# --prompt-mode prompt-mode +# --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. -# --simple-prompt Simple prompt mode +# --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. -# -v, --version Print the version of irb +# --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 # # == Configuration # @@ -463,7 +472,7 @@ module IRB conf[:IRB_RC].call(context) if conf[:IRB_RC] conf[:MAIN_CONTEXT] = context - trap("SIGINT") do + prev_trap = trap("SIGINT") do signal_handle end @@ -472,6 +481,7 @@ module IRB eval_input end ensure + trap("SIGINT", prev_trap) conf[:AT_EXIT].each{|hook| hook.call} end end diff --git a/lib/irb/cmd/ls.rb b/lib/irb/cmd/ls.rb new file mode 100644 index 0000000000..f163f4f9e6 --- /dev/null +++ b/lib/irb/cmd/ls.rb @@ -0,0 +1,83 @@ +# 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) + o.dump("#{klass}.methods", obj.singleton_methods(false)) + o.dump("#{klass}#methods", klass.public_instance_methods(false)) + o.dump("instance variables", obj.instance_variables) + o.dump("class variables", klass.class_variables) + o.dump("locals", locals) + 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/nop.rb b/lib/irb/cmd/nop.rb index fa3c011b5f..d6f7a611a6 100644 --- a/lib/irb/cmd/nop.rb +++ b/lib/irb/cmd/nop.rb @@ -14,10 +14,16 @@ module IRB module ExtendCommand class Nop - - def self.execute(conf, *opts, &block) - command = new(conf) - command.execute(*opts, &block) + 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) diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb new file mode 100644 index 0000000000..0bd40b7d4e --- /dev/null +++ b/lib/irb/cmd/show_source.rb @@ -0,0 +1,86 @@ +# 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 + code = +"" + File.read(file).lines[(first_line - 1)..-1].each_with_index do |line, i| + _ltype, _indent, continue, code_block_open = lex.check_state(code << line) + if !continue && !code_block_open + return first_line + i + 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/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 index a054bb20f8..cfbb3cc668 100644 --- a/lib/irb/color.rb +++ b/lib/irb/color.rb @@ -64,6 +64,7 @@ module IRB # :nodoc: 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 @@ -120,6 +121,7 @@ module IRB # :nodoc: 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 @@ -138,10 +140,11 @@ module IRB # :nodoc: end end length += str.bytesize + end_seen = true if token == :on___end__ end # give up colorizing incomplete Ripper tokens - if length != code.bytesize + unless end_seen or length == code.bytesize return Reline::Unicode.escape_for_print(code) end diff --git a/lib/irb/color_printer.rb b/lib/irb/color_printer.rb index 92afea51cd..30c6825750 100644 --- a/lib/irb/color_printer.rb +++ b/lib/irb/color_printer.rb @@ -21,6 +21,15 @@ module IRB 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 diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index 22a1ad1d3d..d1bb82122e 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -7,7 +7,7 @@ # From Original Idea of shugo@ruby-lang.org # -autoload :RDoc, "rdoc" +require_relative 'ruby-lex' module IRB module InputCompletor # :nodoc: @@ -38,8 +38,69 @@ module IRB BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{(" - CompletionProc = proc { |input| - retrieve_completion_data(input).compact.map{ |i| i.encode(Encoding.default_external) } + def self.retrieve_files_to_require_from_load_path + @@files_from_load_path ||= $LOAD_PATH.flat_map { |path| + begin + Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: path) + rescue Errno::ENOENT + [] + end + }.uniq.map { |path| + path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '') + } + 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) @@ -266,13 +327,22 @@ module IRB 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| diff --git a/lib/irb/ext/save-history.rb b/lib/irb/ext/save-history.rb index ac358c8ccb..7acaebe36a 100644 --- a/lib/irb/ext/save-history.rb +++ b/lib/irb/ext/save-history.rb @@ -81,6 +81,8 @@ module IRB end } end + @loaded_history_lines = history.size + @loaded_history_mtime = File.mtime(history_file) end end @@ -105,12 +107,20 @@ module IRB raise end - open(history_file, "w:#{IRB.conf[:LC_MESSAGES].encoding}", 0600) do |f| + 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") } - 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 + 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 diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb index 347323247e..339e9e6084 100644 --- a/lib/irb/extend-command.rb +++ b/lib/irb/extend-command.rb @@ -126,7 +126,23 @@ module IRB # :nodoc: ], [ - :measure, :Measure, "irb/cmd/measure" + :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], ], ] @@ -168,12 +184,13 @@ module IRB # :nodoc: end if load_file + kwargs = ", **kwargs" if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.7.0" line = __LINE__; eval %[ - def #{cmd_name}(*opts, &b) + 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" if arity < 0 + args << "*opts#{kwargs}" if arity < 0 args << "&block" args = args.join(", ") line = __LINE__; eval %[ @@ -184,7 +201,7 @@ module IRB # :nodoc: end end ], nil, __FILE__, line - __send__ :#{cmd_name}_, *opts, &b + __send__ :#{cmd_name}_, *opts#{kwargs}, &b end ], nil, __FILE__, line else diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index e223672985..1854567a2d 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -280,6 +280,7 @@ module IRB 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] diff --git a/lib/irb/irb.gemspec b/lib/irb/irb.gemspec index 9842b4bce1..38fee9d9fb 100644 --- a/lib/irb/irb.gemspec +++ b/lib/irb/irb.gemspec @@ -28,53 +28,8 @@ Gem::Specification.new do |spec| "doc/irb/irb.rd.ja", "exe/irb", "irb.gemspec", - "lib/irb.rb", - "lib/irb/cmd/chws.rb", - "lib/irb/cmd/fork.rb", - "lib/irb/cmd/help.rb", - "lib/irb/cmd/info.rb", - "lib/irb/cmd/load.rb", - "lib/irb/cmd/measure.rb", - "lib/irb/cmd/nop.rb", - "lib/irb/cmd/pushws.rb", - "lib/irb/cmd/subirb.rb", - "lib/irb/color.rb", - "lib/irb/color_printer.rb", - "lib/irb/completion.rb", - "lib/irb/context.rb", - "lib/irb/easter-egg.rb", - "lib/irb/ext/change-ws.rb", - "lib/irb/ext/history.rb", - "lib/irb/ext/loader.rb", - "lib/irb/ext/multi-irb.rb", - "lib/irb/ext/save-history.rb", - "lib/irb/ext/tracer.rb", - "lib/irb/ext/use-loader.rb", - "lib/irb/ext/workspaces.rb", - "lib/irb/extend-command.rb", - "lib/irb/frame.rb", - "lib/irb/help.rb", - "lib/irb/init.rb", - "lib/irb/input-method.rb", - "lib/irb/inspector.rb", - "lib/irb/lc/error.rb", - "lib/irb/lc/help-message", - "lib/irb/lc/ja/encoding_aliases.rb", - "lib/irb/lc/ja/error.rb", - "lib/irb/lc/ja/help-message", - "lib/irb/locale.rb", - "lib/irb/magic-file.rb", - "lib/irb/notifier.rb", - "lib/irb/output-method.rb", - "lib/irb/ruby-lex.rb", - "lib/irb/ruby_logo.aa", - "lib/irb/src_encoding.rb", - "lib/irb/version.rb", - "lib/irb/workspace.rb", - "lib/irb/ws-for-case-2.rb", - "lib/irb/xmp.rb", "man/irb.1", - ] + ] + Dir.glob("lib/**/*") spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] diff --git a/lib/irb/lc/help-message b/lib/irb/lc/help-message index a80facc9c5..9c3ea859ad 100644 --- a/lib/irb/lc/help-message +++ b/lib/irb/lc/help-message @@ -10,7 +10,7 @@ # # Usage: irb.rb [options] [programfile] [arguments] - -f Suppress read of ~/.irbrc + -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 @@ -18,7 +18,7 @@ Usage: irb.rb [options] [programfile] [arguments] -E enc Same as `ruby -E` -w Same as `ruby -w` -W[level=2] Same as `ruby -W` - --context-mode n Set n[0-3] to method to create Binding Object, + --context-mode n Set n[0-4] to method to create Binding Object, when new workspace was created --echo Show result(default) --noecho Don't show result @@ -31,8 +31,8 @@ Usage: irb.rb [options] [programfile] [arguments] --colorize Use colorization --nocolorize Don't use colorization --prompt prompt-mode/--prompt-mode prompt-mode - Switch prompt mode. Pre-defined prompt modes are - `default', `simple', `xmp' and `inf-ruby' + 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 @@ -41,8 +41,8 @@ Usage: irb.rb [options] [programfile] [arguments] --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. + 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 diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index ce94797dad..82df06da2b 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -47,12 +47,26 @@ class RubyLex @io = io if @io.respond_to?(:check_termination) @io.check_termination do |code| - code.gsub!(/\s*\z/, '').concat("\n") - ltype, indent, continue, code_block_open = check_state(code) - if ltype or indent > 0 or continue or code_block_open - false + if Reline::IOGate.in_pasting? + lex = RubyLex.new + rest = lex.check_termination_in_prev_line(code) + if rest + Reline.delete_text + rest.bytes.reverse_each do |c| + Reline.ungetc(c) + end + true + else + false + end else - true + code.gsub!(/\s*\z/, '').concat("\n") + ltype, indent, continue, code_block_open = check_state(code) + if ltype or indent > 0 or continue or code_block_open + false + else + true + end end end end @@ -60,7 +74,7 @@ class RubyLex @io.dynamic_prompt do |lines| lines << '' if lines.empty? result = [] - tokens = ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join) + tokens = self.class.ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join) code = String.new partial_tokens = [] unprocessed_tokens = [] @@ -115,10 +129,10 @@ class RubyLex :on_param_error ] - def ripper_lex_without_warning(code) + def self.ripper_lex_without_warning(code) verbose, $VERBOSE = $VERBOSE, nil tokens = nil - self.class.compile_with_errors_suppressed(code) do |inner_code, line_no| + compile_with_errors_suppressed(code) do |inner_code, line_no| lexer = Ripper::Lexer.new(inner_code, '-', line_no) if lexer.respond_to?(:scan) # Ruby 2.7+ tokens = [] @@ -168,7 +182,7 @@ class RubyLex 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 = ripper_lex_without_warning(lines[0..line_index].join("\n")) + @tokens = self.class.ripper_lex_without_warning(lines[0..line_index].join("\n")) prev_spaces = find_prev_spaces(line_index) depth_difference = check_newline_depth_difference depth_difference = 0 if depth_difference < 0 @@ -177,7 +191,7 @@ class RubyLex 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 = ripper_lex_without_warning(code) + @tokens = self.class.ripper_lex_without_warning(code) corresponding_token_depth = check_corresponding_token_depth if corresponding_token_depth corresponding_token_depth @@ -190,7 +204,7 @@ class RubyLex end def check_state(code, tokens = nil) - tokens = ripper_lex_without_warning(code) unless tokens + tokens = self.class.ripper_lex_without_warning(code) unless tokens ltype = process_literal_type(tokens) indent = process_nesting_level(tokens) continue = process_continue(tokens) @@ -256,7 +270,7 @@ class RubyLex end code = @line + (line.nil? ? '' : line) code.gsub!(/\s*\z/, '').concat("\n") - @tokens = ripper_lex_without_warning(code) + @tokens = self.class.ripper_lex_without_warning(code) @continue = process_continue @code_block_open = check_code_block(code) @indent = process_nesting_level @@ -277,8 +291,9 @@ class RubyLex return true elsif tokens.size >= 1 and tokens[-1][1] == :on_heredoc_end # "EOH\n" return false - elsif tokens.size >= 2 and defined?(Ripper::EXPR_BEG) and tokens[-2][3].anybits?(Ripper::EXPR_BEG | Ripper::EXPR_FNAME) + elsif tokens.size >= 2 and defined?(Ripper::EXPR_BEG) and tokens[-2][3].anybits?(Ripper::EXPR_BEG | Ripper::EXPR_FNAME) and tokens[-2][2] !~ /\A\.\.\.?\z/ # end of literal except for regexp + # endless range at end of line is not a continue return true end false @@ -738,5 +753,50 @@ class RubyLex nil end end + + def check_termination_in_prev_line(code) + tokens = self.class.ripper_lex_without_warning(code) + 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 + false + end + end + if index + first_token = nil + last_line_tokens = tokens[(index + 1)..(tokens.size - 1)] + last_line_tokens.each do |t| + unless [:on_sp, :on_ignored_sp, :on_comment].include?(t.event) + first_token = t + break + end + end + if first_token.nil? + return false + elsif first_token && first_token.state == Ripper::EXPR_DOT + return false + else + tokens_without_last_line = tokens[0..index] + ltype = process_literal_type(tokens_without_last_line) + indent = process_nesting_level(tokens_without_last_line) + continue = process_continue(tokens_without_last_line) + code_block_open = check_code_block(tokens_without_last_line.map(&:tok).join(''), tokens_without_last_line) + if ltype or indent > 0 or continue or code_block_open + return false + else + return last_line_tokens.map(&:tok).join('') + end + end + end + false + end end # :startdoc: diff --git a/lib/irb/version.rb b/lib/irb/version.rb index 0a4a1bb922..0014bdda74 100644 --- a/lib/irb/version.rb +++ b/lib/irb/version.rb @@ -11,7 +11,7 @@ # module IRB # :nodoc: - VERSION = "1.3.4" + VERSION = "1.3.5" @RELEASE_VERSION = VERSION - @LAST_UPDATE_DATE = "2021-02-25" + @LAST_UPDATE_DATE = "2021-04-03" end diff --git a/lib/reline.rb b/lib/reline.rb index 81ea9f9b58..a7bd4d9280 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -446,6 +446,10 @@ module Reline } end + def self.ungetc(c) + Reline::IOGate.ungetc(c) + end + def self.line_editor core.line_editor end diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 12a2bde234..7d71e62d63 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -813,6 +813,7 @@ class Reline::LineEditor end move_cursor_up(back) move_cursor_down(@first_line_started_from + @started_from) + @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end @@ -1158,8 +1159,25 @@ class Reline::LineEditor def call_completion_proc result = retrieve_completion_block(true) - slice = result[1] - result = @completion_proc.(slice) if @completion_proc and slice + preposing, target, postposing = result + if @completion_proc and target + argnum = @completion_proc.parameters.inject(0) { |result, item| + case item.first + when :req, :opt + result + 1 + when :rest + break 3 + end + } + case argnum + when 1 + result = @completion_proc.(target) + when 2 + result = @completion_proc.(target, preposing) + when 3..Float::INFINITY + result = @completion_proc.(target, preposing, postposing) + end + end Reline.core.instance_variable_set(:@completion_quote_character, nil) result end @@ -1207,8 +1225,16 @@ class Reline::LineEditor end def retrieve_completion_block(set_completion_quote_character = false) - word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ - quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ + if Reline.completer_word_break_characters.empty? + word_break_regexp = nil + else + word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ + end + if Reline.completer_quote_characters.empty? + quote_characters_regexp = nil + else + quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ + end before = @line.byteslice(0, @byte_pointer) rest = nil break_pointer = nil @@ -1229,14 +1255,14 @@ class Reline::LineEditor elsif quote and slice.start_with?(escaped_quote) # skip i += 2 - elsif slice =~ quote_characters_regexp # find new " + elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new " rest = $' quote = $& closing_quote = /(?!\\)#{Regexp.escape(quote)}/ escaped_quote = /\\#{Regexp.escape(quote)}/ i += 1 break_pointer = i - 1 - elsif not quote and slice =~ word_break_regexp + elsif word_break_regexp and not quote and slice =~ word_break_regexp rest = $' i += 1 before = @line.byteslice(i, @byte_pointer - i) @@ -1264,6 +1290,19 @@ class Reline::LineEditor end target = before end + if @is_multiline + if @previous_line_index + lines = whole_lines(index: @previous_line_index, line: @line) + else + lines = whole_lines + end + if @line_index > 0 + preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing + end + if (lines.size - 1) > @line_index + postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n") + end + end [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)] end @@ -1291,10 +1330,32 @@ class Reline::LineEditor def delete_text(start = nil, length = nil) if start.nil? and length.nil? - @line&.clear - @byte_pointer = 0 - @cursor = 0 - @cursor_max = 0 + if @is_multiline + if @buffer_of_lines.size == 1 + @line&.clear + @byte_pointer = 0 + @cursor = 0 + @cursor_max = 0 + elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0 + @buffer_of_lines.pop + @line_index -= 1 + @line = @buffer_of_lines[@line_index] + @byte_pointer = 0 + @cursor = 0 + @cursor_max = calculate_width(@line) + elsif @line_index < (@buffer_of_lines.size - 1) + @buffer_of_lines.delete_at(@line_index) + @line = @buffer_of_lines[@line_index] + @byte_pointer = 0 + @cursor = 0 + @cursor_max = calculate_width(@line) + end + else + @line&.clear + @byte_pointer = 0 + @cursor = 0 + @cursor_max = 0 + end elsif not start.nil? and not length.nil? if @line before = @line.byteslice(0, start) diff --git a/lib/reline/version.rb b/lib/reline/version.rb index 11e8145c7f..44db465a2f 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.2.4' + VERSION = '0.2.5' end diff --git a/test/irb/test_cmd.rb b/test/irb/test_cmd.rb index 41f84f1922..044d852a32 100644 --- a/test/irb/test_cmd.rb +++ b/test/irb/test_cmd.rb @@ -5,6 +5,32 @@ require "irb/extend-command" module TestIRB class ExtendCommand < Test::Unit::TestCase + class TestInputMethod < ::IRB::InputMethod + attr_reader :list, :line_no + + def initialize(list = []) + super("test") + @line_no = 0 + @list = list + end + + def gets + @list[@line_no]&.tap {@line_no += 1} + end + + def eof? + @line_no >= @list.size + end + + def encoding + Encoding.default_external + end + + def reset + @line_no = 0 + end + end + def setup @pwd = Dir.pwd @tmpdir = File.join(Dir.tmpdir, "test_reline_config_#{$$}") @@ -17,12 +43,14 @@ module TestIRB Dir.chdir(@tmpdir) @home_backup = ENV["HOME"] ENV["HOME"] = @tmpdir + @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") @default_encoding = [Encoding.default_external, Encoding.default_internal] @stdio_encodings = [STDIN, STDOUT, STDERR].map {|io| [io.external_encoding, io.internal_encoding] } IRB.instance_variable_get(:@CONF).clear end def teardown + ENV["XDG_CONFIG_HOME"] = @xdg_config_home_backup ENV["HOME"] = @home_backup Dir.chdir(@pwd) FileUtils.rm_rf(@tmpdir) @@ -42,12 +70,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = false IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReidlineInputMethod\swith\sReline .+ and .+\n + InputMethod:\sAbstract\sInputMethod\n \.irbrc\spath: .+\n RUBY_PLATFORM: .+ }x @@ -62,12 +90,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = true IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReadlineInputMethod\swith .+ and .+\n + InputMethod:\sAbstract\sInputMethod\n \.irbrc\spath: .+\n RUBY_PLATFORM: .+ }x @@ -85,12 +113,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = false IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReidlineInputMethod\swith\sReline\s[^ ]+(?!\sand\s.+)\n + InputMethod:\sAbstract\sInputMethod\n RUBY_PLATFORM: .+\n \z }x @@ -112,12 +140,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = true IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReadlineInputMethod\swith\s(?~.*\sand\s.+)\n + InputMethod:\sAbstract\sInputMethod\n RUBY_PLATFORM: .+\n \z }x @@ -128,32 +156,6 @@ module TestIRB IRB.const_set(:IRBRC_EXT, ext_backup) end - class TestInputMethod < ::IRB::InputMethod - attr_reader :list, :line_no - - def initialize(list = []) - super("test") - @line_no = 0 - @list = list - end - - def gets - @list[@line_no]&.tap {@line_no += 1} - end - - def eof? - @line_no >= @list.size - end - - def encoding - Encoding.default_external - end - - def reset - @line_no = 0 - end - end - def test_measure IRB.init_config(nil) IRB.conf[:PROMPT] = { @@ -372,5 +374,56 @@ module TestIRB /=> "bug17564"\n/, ], out) end + + def test_ls + input = TestInputMethod.new([ + "ls Object.new.tap { |o| o.instance_variable_set(:@a, 1) }\n", + ]) + IRB.init_config(nil) + workspace = IRB::WorkSpace.new(self) + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(workspace, input) + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(/^instance variables:\s+@a\n/m, out) + end + + def test_show_source + input = TestInputMethod.new([ + "show_source 'IRB.conf'\n", + ]) + IRB.init_config(nil) + workspace = IRB::WorkSpace.new(self) + irb = IRB::Irb.new(workspace, input) + IRB.conf[:VERBOSE] = false + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(%r[/irb\.rb], out) + end + + def test_whereami + input = TestInputMethod.new([ + "whereami\n", + ]) + IRB.init_config(nil) + workspace = IRB::WorkSpace.new(self) + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(workspace, input) + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(/^From: .+ @ line \d+ :\n/, out) + end end end diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index 9976008124..a28ae06117 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -66,6 +66,7 @@ module TestIRB "\t" => "\t", # not ^I "foo(*%W(bar))" => "foo(*#{RED}#{BOLD}%W(#{CLEAR}#{RED}bar#{CLEAR}#{RED}#{BOLD})#{CLEAR})", "$stdout" => "#{GREEN}#{BOLD}$stdout#{CLEAR}", + "__END__" => "#{GREEN}__END__#{CLEAR}", } # specific to Ruby 2.7+ diff --git a/test/irb/test_color_printer.rb b/test/irb/test_color_printer.rb index 1b28837658..1afc7ccf55 100644 --- a/test/irb/test_color_printer.rb +++ b/test/irb/test_color_printer.rb @@ -34,6 +34,7 @@ module TestIRB end { 1 => "#{BLUE}#{BOLD}1#{CLEAR}\n", + "a\nb" => %[#{RED}#{BOLD}"#{CLEAR}#{RED}a\\nb#{CLEAR}#{RED}#{BOLD}"#{CLEAR}\n], IRBTestColorPrinter.new('test') => "#{GREEN}#<struct TestIRB::TestColorPrinter::IRBTestColorPrinter#{CLEAR} a#{GREEN}=#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{RED}test#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{GREEN}>#{CLEAR}\n", Ripper::Lexer.new('1').scan => "[#{GREEN}#<Ripper::Lexer::Elem:#{CLEAR} on_int@1:0 END token: #{RED}#{BOLD}\"#{CLEAR}#{RED}1#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{GREEN}>#{CLEAR}]\n", Class.new{define_method(:pretty_print){|q| q.text("[__FILE__, __LINE__, __ENCODING__]")}}.new => "[#{CYAN}#{BOLD}__FILE__#{CLEAR}, #{CYAN}#{BOLD}__LINE__#{CLEAR}, #{CYAN}#{BOLD}__ENCODING__#{CLEAR}]\n", diff --git a/test/irb/test_completion.rb b/test/irb/test_completion.rb index 984453d059..535690ae22 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -55,5 +55,33 @@ module TestIRB namespace = IRB::InputCompletor.retrieve_completion_data("1.positive?", bind: binding, doc_namespace: true) assert_equal "Integer.positive?", namespace end + + def test_complete_require + candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + %w['irb/init 'irb/ruby-lex].each do |word| + assert_include candidates, word + end + # Test cache + candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + %w['irb/init 'irb/ruby-lex].each do |word| + assert_include candidates, word + end + end + + def test_complete_require_relative + candidates = Dir.chdir(__dir__ + "/../..") do + IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + end + %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| + assert_include candidates, word + end + # Test cache + candidates = Dir.chdir(__dir__ + "/../..") do + IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + end + %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| + assert_include candidates, word + end + end end end diff --git a/test/irb/test_history.rb b/test/irb/test_history.rb index 392a6afa9a..81b7fe8679 100644 --- a/test/irb/test_history.rb +++ b/test/irb/test_history.rb @@ -127,11 +127,43 @@ module TestIRB INPUT end + def test_history_concurrent_use + omit "Skip Editline" if /EditLine/n.match(Readline::VERSION) + IRB.conf[:SAVE_HISTORY] = 1 + assert_history(<<~EXPECTED_HISTORY, <<~INITIAL_HISTORY, <<~INPUT) do |history_file| + exit + 5 + exit + EXPECTED_HISTORY + 1 + 2 + 3 + 4 + INITIAL_HISTORY + 5 + exit + INPUT + assert_history(<<~EXPECTED_HISTORY2, <<~INITIAL_HISTORY2, <<~INPUT2) + exit + EXPECTED_HISTORY2 + 1 + 2 + 3 + 4 + INITIAL_HISTORY2 + 5 + exit + INPUT2 + File.utime(File.atime(history_file), File.mtime(history_file) + 2, history_file) + end + end + private def assert_history(expected_history, initial_irb_history, input) backup_verbose, $VERBOSE = $VERBOSE, nil backup_home = ENV["HOME"] + backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") IRB.conf[:LC_MESSAGES] = IRB::Locale.new actual_history = nil Dir.mktmpdir("test_irb_history_#{$$}") do |tmpdir| @@ -143,6 +175,11 @@ module TestIRB io = TestInputMethod.new io.class::HISTORY.clear io.load_history + if block_given? + history = io.class::HISTORY.dup + yield IRB.rc_file("_history") + io.class::HISTORY.replace(history) + end io.class::HISTORY.concat(input.split) io.save_history @@ -160,6 +197,7 @@ module TestIRB ensure $VERBOSE = backup_verbose ENV["HOME"] = backup_home + ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home end def with_temp_stdio diff --git a/test/irb/test_init.rb b/test/irb/test_init.rb index 83b4b5a543..2c50b5da3a 100644 --- a/test/irb/test_init.rb +++ b/test/irb/test_init.rb @@ -64,6 +64,12 @@ module TestIRB ENV["IRBRC"] = backup_irbrc end + def test_recovery_sigint + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e binding.irb;loop{Process.kill("SIGINT",$$)} -- -f --], "exit\n", //, //) + Process.kill("SIGKILL", status.pid) if !status.exited? && !status.stopped? && !status.signaled? + end + private def with_argv(argv) diff --git a/test/irb/test_ruby_lex.rb b/test/irb/test_ruby_lex.rb index a45ca668b9..556afbd776 100644 --- a/test/irb/test_ruby_lex.rb +++ b/test/irb/test_ruby_lex.rb @@ -136,6 +136,20 @@ module TestIRB end end + def test_endless_range_at_end_of_line + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6.0') + skip 'Endless range is available in 2.6.0 or later' + end + input_with_prompt = [ + PromptRow.new('001:0: :> ', %q(a = 3..)), + PromptRow.new('002:0: :* ', %q()), + ] + + lines = input_with_prompt.map(&:content) + expected_prompt_list = input_with_prompt.map(&:prompt) + assert_dynamic_prompt(lines, expected_prompt_list) + end + def test_incomplete_coding_magic_comment input_with_correct_indents = [ Row.new(%q(#coding:u), nil, 0), @@ -544,8 +558,7 @@ module TestIRB skip 'This test needs Ripper::Lexer#scan to take broken tokens' end - ruby_lex = RubyLex.new - tokens = ruby_lex.ripper_lex_without_warning('%wwww') + tokens = RubyLex.ripper_lex_without_warning('%wwww') pos_to_index = {} tokens.each_with_index { |t, i| assert_nil(pos_to_index[t[0]], "There is already another token in the position of #{t.inspect}.") @@ -558,8 +571,7 @@ module TestIRB skip 'This test needs Ripper::Lexer#scan to take broken tokens' end - ruby_lex = RubyLex.new - tokens = ruby_lex.ripper_lex_without_warning(<<~EOC.chomp) + tokens = RubyLex.ripper_lex_without_warning(<<~EOC.chomp) def foo %wwww end diff --git a/test/irb/yamatanooroti/test_rendering.rb b/test/irb/yamatanooroti/test_rendering.rb new file mode 100644 index 0000000000..8f55b38a93 --- /dev/null +++ b/test/irb/yamatanooroti/test_rendering.rb @@ -0,0 +1,165 @@ +require 'irb' + +begin + require 'yamatanooroti' + + class IRB::TestRendering < Yamatanooroti::TestCase + def setup + @pwd = Dir.pwd + suffix = '%010d' % Random.rand(0..65535) + @tmpdir = File.join(File.expand_path(Dir.tmpdir), "test_irb_#{$$}_#{suffix}") + begin + Dir.mkdir(@tmpdir) + rescue Errno::EEXIST + FileUtils.rm_rf(@tmpdir) + Dir.mkdir(@tmpdir) + end + @irbrc_backup = ENV['IRBRC'] + @irbrc_file = ENV['IRBRC'] = File.join(@tmpdir, 'temporaty_irbrc') + File.unlink(@irbrc_file) if File.exist?(@irbrc_file) + end + + def teardown + FileUtils.rm_rf(@tmpdir) + ENV['IRBRC'] = @irbrc_backup + ENV.delete('RELINE_TEST_PROMPT') if ENV['RELINE_TEST_PROMPT'] + end + + def test_launch + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + 'Hello, World!' + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001:0> 'Hello, World!' + => "Hello, World!" + irb(main):002:0> + EOC + end + + def test_multiline_paste + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + class A + def inspect; '#<A>'; end + def a; self; end + def b; true; end + end + + a = A.new + + a + .a + .b + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001:1* class A + irb(main):002:1* def inspect; '#<A>'; end + irb(main):003:1* def a; self; end + irb(main):004:1* def b; true; end + irb(main):005:0> end + => :b + irb(main):006:0> + irb(main):007:0> a = A.new + => #<A> + irb(main):008:0> + irb(main):009:0> a + irb(main):010:0> .a + irb(main):011:0> .b + => true + irb(main):012:0> + EOC + end + + def test_evaluate_each_toplevel_statement_by_multiline_paste + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + class A + def inspect; '#<A>'; end + def b; self; end + def c; true; end + end + + a = A.new + + a + .b + # aaa + .c + + (a) + &.b() + + + class A def b; self; end; def c; true; end; end; + a = A.new + a + .b + # aaa + .c + (a) + &.b() + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001:1* class A + irb(main):002:1* def inspect; '#<A>'; end + irb(main):003:1* def b; self; end + irb(main):004:1* def c; true; end + irb(main):005:0> end + => :c + irb(main):006:0> + irb(main):007:0> a = A.new + => #<A> + irb(main):008:0> + irb(main):009:0> a + irb(main):010:0> .b + irb(main):011:0> # aaa + irb(main):012:0> .c + => true + irb(main):013:0> + irb(main):014:0> (a) + irb(main):015:0> &.b() + => #<A> + irb(main):016:0> + irb(main):017:0> + irb(main):018:0> class A def b; self; end; def c; true; end; end; + => :c + irb(main):019:0> a = A.new + => #<A> + irb(main):020:0> a + irb(main):021:0> .b + irb(main):022:0> # aaa + irb(main):023:0> .c + => true + irb(main):024:0> (a) + irb(main):025:0> &.b() + => #<A> + irb(main):026:0> + EOC + end + + private def write_irbrc(content) + File.open(@irbrc_file, 'w') do |f| + f.write content + end + end + end +rescue LoadError, NameError + # On Ruby repository, this test suit doesn't run because Ruby repo doesn't + # have the yamatanooroti gem. +end diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index d2de4690d5..0f32ec4421 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -65,6 +65,8 @@ class Reline::Test < Reline::TestCase Reline.completer_word_break_characters = "[".encode(Encoding::ASCII) assert_equal("[", Reline.completer_word_break_characters) assert_equal(get_reline_encoding, Reline.completer_word_break_characters.encoding) + + assert_nothing_raised { Reline.completer_word_break_characters = '' } ensure Reline.completer_word_break_characters = completer_word_break_characters end @@ -89,6 +91,8 @@ class Reline::Test < Reline::TestCase Reline.completer_quote_characters = "`".encode(Encoding::ASCII) assert_equal("`", Reline.completer_quote_characters) assert_equal(get_reline_encoding, Reline.completer_quote_characters.encoding) + + assert_nothing_raised { Reline.completer_quote_characters = '' } ensure Reline.completer_quote_characters = completer_quote_characters end diff --git a/test/reline/test_string_processing.rb b/test/reline/test_string_processing.rb index e76fa384f2..0e0ee9cc04 100644 --- a/test/reline/test_string_processing.rb +++ b/test/reline/test_string_processing.rb @@ -20,4 +20,58 @@ class Reline::LineEditor::StringProcessingTest < Reline::TestCase width = @line_editor.send(:calculate_width, "\1\e[31m\2RubyColor\1\e[34m\2 default string \1\e[m\2>", true) assert_equal('RubyColor default string >'.size, width) end + + def test_completion_proc_with_preposing_and_postposing + buf = ['def hoge', ' puts :aaa', 'end'] + + @line_editor.instance_variable_set(:@is_multiline, true) + @line_editor.instance_variable_set(:@buffer_of_lines, buf) + @line_editor.instance_variable_set(:@line, buf[1]) + @line_editor.instance_variable_set(:@byte_pointer, 3) + @line_editor.instance_variable_set(:@cursor, 3) + @line_editor.instance_variable_set(:@cursor_max, 11) + @line_editor.instance_variable_set(:@line_index, 1) + @line_editor.instance_variable_set(:@completion_proc, proc { |target| + assert_equal('p', target) + }) + @line_editor.__send__(:call_completion_proc) + + @line_editor.instance_variable_set(:@is_multiline, true) + @line_editor.instance_variable_set(:@buffer_of_lines, buf) + @line_editor.instance_variable_set(:@line, buf[1]) + @line_editor.instance_variable_set(:@byte_pointer, 6) + @line_editor.instance_variable_set(:@cursor, 6) + @line_editor.instance_variable_set(:@cursor_max, 11) + @line_editor.instance_variable_set(:@line_index, 1) + @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| + assert_equal('puts', target) + assert_equal("def hoge\n ", pre) + assert_equal(" :aaa\nend", post) + }) + @line_editor.__send__(:call_completion_proc) + + @line_editor.instance_variable_set(:@line, buf[0]) + @line_editor.instance_variable_set(:@byte_pointer, 6) + @line_editor.instance_variable_set(:@cursor, 6) + @line_editor.instance_variable_set(:@cursor_max, 8) + @line_editor.instance_variable_set(:@line_index, 0) + @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| + assert_equal('ho', target) + assert_equal('def ', pre) + assert_equal("ge\n puts :aaa\nend", post) + }) + @line_editor.__send__(:call_completion_proc) + + @line_editor.instance_variable_set(:@line, buf[2]) + @line_editor.instance_variable_set(:@byte_pointer, 1) + @line_editor.instance_variable_set(:@cursor, 1) + @line_editor.instance_variable_set(:@cursor_max, 3) + @line_editor.instance_variable_set(:@line_index, 2) + @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| + assert_equal('e', target) + assert_equal("def hoge\n puts :aaa\n", pre) + assert_equal('nd', post) + }) + @line_editor.__send__(:call_completion_proc) + end end diff --git a/test/reline/test_within_pipe.rb b/test/reline/test_within_pipe.rb index 70a0e0a5de..e453b1902e 100644 --- a/test/reline/test_within_pipe.rb +++ b/test/reline/test_within_pipe.rb @@ -59,4 +59,17 @@ class Reline::WithinPipeTest < Reline::TestCase @writer.write("abcde\C-b\C-b\C-b\C-x\C-d\C-x\C-h\C-x\C-v\C-a\C-f\C-f EF\C-x\C-t gh\C-x\M-t\C-b\C-b\C-b\C-b\C-b\C-b\C-b\C-b\C-x\M-u\C-x\M-l\C-x\M-c\n") assert_equal "a\C-aDE gh Fe", Reline.readmultiline(&proc{ true }) end + + def test_delete_text_in_multiline + @writer.write("abc\ndef\nxyz\n") + result = Reline.readmultiline(&proc{ |str| + if str.include?('xyz') + Reline.delete_text + true + else + false + end + }) + assert_equal "abc\ndef", result + end end diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 6f9a14de67..13693e7c4d 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -719,6 +719,17 @@ begin EOC end + def test_reset_rest_height_when_clear_screen + start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') + write("\n\n\n\C-l3\n") + close + assert_screen(<<~EOC) + prompt> 3 + => 3 + prompt> + EOC + end + private def write_inputrc(content) File.open(@inputrc_file, 'w') do |f| f.write content |