From 77700bf023a963af810bcc49184428a75cd23bb1 Mon Sep 17 00:00:00 2001 From: aycabta Date: Sun, 7 Feb 2021 21:04:32 +0900 Subject: [PATCH] Backport lib/reline, and lib/irb for 3.0.1 2nd (#4157) * [ruby/irb] Stub a screen size for tests https://github.com/ruby/irb/commit/6663057083 * [ruby/irb] Support GitHub Actions https://github.com/ruby/irb/commit/8e9e6c4037 * [ruby/irb] Stub a screen size for test_context http://ci.rvm.jp/logfiles/brlog.trunk-random1.20210119-074232 https://github.com/ruby/irb/commit/ea87592d4a * [ruby/irb] Use a real screen size for pp by default https://github.com/ruby/irb/commit/9b9300dec2 * [ruby/irb] Rescue Errno::EINVAL on IRB pp http://rubyci.s3.amazonaws.com/solaris11-gcc/ruby-master/log/20210119T070008Z.log.html.gz is caused by: /export/home/chkbuild/chkbuild-gcc/tmp/build/20210119T150010Z/ruby/lib/reline/ansi.rb:157:in `winsize': Invalid argument - (Errno::EINVAL) from /export/home/chkbuild/chkbuild-gcc/tmp/build/20210119T150010Z/ruby/lib/reline/ansi.rb:157:in `get_screen_size' from /export/home/chkbuild/chkbuild-gcc/tmp/build/20210119T150010Z/ruby/lib/reline.rb:168:in `get_screen_size' from /export/home/chkbuild/chkbuild-gcc/tmp/build/20210119T150010Z/ruby/lib/forwardable.rb:238:in `get_screen_size' from /export/home/chkbuild/chkbuild-gcc/tmp/build/20210119T150010Z/ruby/lib/irb/color_printer.rb:7:in `pp' from -e:1:in `
' https://github.com/ruby/irb/commit/1719514598 * [ruby/irb] Split test files for IRB::Color and IRB::ColorPrinter https://github.com/ruby/irb/commit/d95e8daab3 * [ruby/irb] Undefine unused constants https://github.com/ruby/irb/commit/eea9c16804 * [ruby/irb] Remove pp-specific stub from TestColor because it was for TestColorPrinter https://github.com/ruby/irb/commit/7569206fd4 * [ruby/irb] Delete a doodle-level memo comment... https://github.com/ruby/irb/commit/fc3e1d9e0c * [ruby/irb] Indent correctly with keyword "for" and "in" https://github.com/ruby/irb/commit/47c83ea724 * [ruby/irb] Indent correctly with method calling with receiver https://github.com/ruby/irb/commit/e7c68e74a0 * [ruby/irb] add `IRB::FileInputMethod.open` to ensure closing associated File * tweak some methods not to raise exception after `#close` * use it in `IRB::IrbLoader#{source_file,load_file} https://github.com/ruby/irb/commit/ec2947acbd * [ruby/irb] use `RubyLex::TerminateLineInput` appropriately [Bug #17564] * using the appropriciate exception instead of `break` so that the session can be continue after the `irb_source` and `irb_load` commands * suppress extra new line due to one more `#prompt` call https://github.com/ruby/irb/commit/bdefaa7cfd * [ruby/irb] specify the `VERBOSE` to `false` and fix tests to fit https://github.com/ruby/irb/commit/502c590925 * In test, need to pass a context to IRB::WorkSpace.new explicitly * Fix absolute path predicate on Windows A path starts with '/' is not an absolute path on Windows, because of drive letter or UNC. * [ruby/irb] follow up the actual line number https://github.com/ruby/irb/commit/7aed8fe3b1 * [ruby/irb] Add info.rb to gemspec https://github.com/ruby/irb/commit/adbba19adf * [ruby/irb] Allow "measure" command to take block https://github.com/ruby/irb/commit/20f1ca23e9 * [ruby/irb] Enable to reassign a new block with "measure" command https://github.com/ruby/irb/commit/b444573aa2 * [ruby/reline] Cache pasting state in processing a key Because it's too slow. The rendering time in IRB has been reduced as follows: start = Time.now def each_top_level_statement initialize_input catch(:TERM_INPUT) do loop do begin prompt unless l = lex throw :TERM_INPUT if @line == '' else @line_no += l.count("\n") next if l == "\n" @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 break if @io.eof? @line = '' @exp_line_no = @line_no @indent = 0 rescue TerminateLineInput initialize_input prompt end end end end puts "Duration: #{Time.now - start} seconds" 0.22sec -> 0.14sec https://github.com/ruby/reline/commit/b8b3dd52c0 * [ruby/reline] Initialize uninitialized variables in tests https://github.com/ruby/reline/commit/25af4bb64b * [ruby/reline] Remove an unused variable https://github.com/ruby/reline/commit/123ea51166 * [ruby/reline] Scroll down when ^C is pressed https://github.com/ruby/reline/commit/6877a7e3f5 * [ruby/reline] Show all lines higher than the screen when finished On Unix-like OSes, logs prior to the screen are not editable. When the code is higher than the screen, the code is only shown on the screen until input is finished, but when it is finished, all lines are outputted. https://github.com/ruby/reline/commit/8cd9132a39 * [ruby/reline] Handle past logs correctly when the code is higher than the screen https://github.com/ruby/reline/commit/f197139b4a * [ruby/reline] Update cursor info by inserting newline even if not in pasting https://github.com/ruby/reline/commit/92d314f514 * [ruby/reline] Move cursor just after the last line when finished https://github.com/ruby/reline/commit/ba06e4c480 * [ruby/reline] The vi_histedit supports multiline This closes ruby/reline#253. https://github.com/ruby/reline/commit/f131f86d71 * [ruby/reline] Autowrap correctly when inserting chars in the middle of a line https://github.com/ruby/reline/commit/ebaf37255f * [ruby/reline] Terminate correctly in the middle of lines higher than the screen https://github.com/ruby/reline/commit/e1d9240ada * [ruby/irb] Version 1.3.3 https://github.com/ruby/irb/commit/4c87035b7c * [ruby/reline] Version 0.2.3 https://github.com/ruby/reline/commit/b26c7d60c8 Co-authored-by: Takashi Kokubun Co-authored-by: Nobuhiro IMAI Co-authored-by: Nobuyoshi Nakada Co-authored-by: ima1zumi --- lib/irb.rb | 2 +- lib/irb/cmd/measure.rb | 14 ++- lib/irb/cmd/nop.rb | 4 +- lib/irb/color_printer.rb | 20 +++- lib/irb/ext/loader.rb | 65 +++++++++---- lib/irb/init.rb | 12 ++- lib/irb/input-method.rb | 20 +++- lib/irb/irb.gemspec | 1 + lib/irb/ruby-lex.rb | 64 ++++++++++-- lib/irb/version.rb | 4 +- lib/reline.rb | 2 + lib/reline/line_editor.rb | 102 ++++++++++++++++---- lib/reline/version.rb | 2 +- test/irb/test_cmd.rb | 93 ++++++++++++++++++ test/irb/test_color.rb | 22 ----- test/irb/test_color_printer.rb | 67 +++++++++++++ test/irb/test_context.rb | 38 +++++++- test/irb/test_ruby_lex.rb | 96 ++++++++++++++++++ test/reline/yamatanooroti/test_rendering.rb | 28 ++++++ 19 files changed, 568 insertions(+), 88 deletions(-) create mode 100644 test/irb/test_color_printer.rb diff --git a/lib/irb.rb b/lib/irb.rb index 3f7f169c69..7f99974f28 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -525,7 +525,7 @@ module IRB printf "Use \"exit\" to leave %s\n", @context.ap_name end else - print "\n" + print "\n" if @context.prompting? end end l diff --git a/lib/irb/cmd/measure.rb b/lib/irb/cmd/measure.rb index 5e0bef62af..58eaec2ded 100644 --- a/lib/irb/cmd/measure.rb +++ b/lib/irb/cmd/measure.rb @@ -8,7 +8,7 @@ module IRB super(*args) end - def execute(type = nil, arg = nil) + def execute(type = nil, arg = nil, &block) case type when :off IRB.conf[:MEASURE] = nil @@ -22,9 +22,15 @@ module IRB added = IRB.set_measure_callback(type, arg) 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 + 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 diff --git a/lib/irb/cmd/nop.rb b/lib/irb/cmd/nop.rb index 9cf4337c28..fa3c011b5f 100644 --- a/lib/irb/cmd/nop.rb +++ b/lib/irb/cmd/nop.rb @@ -15,9 +15,9 @@ module IRB class Nop - def self.execute(conf, *opts) + def self.execute(conf, *opts, &block) command = new(conf) - command.execute(*opts) + command.execute(*opts, &block) end def initialize(conf) diff --git a/lib/irb/color_printer.rb b/lib/irb/color_printer.rb index 73a150f881..92afea51cd 100644 --- a/lib/irb/color_printer.rb +++ b/lib/irb/color_printer.rb @@ -4,11 +4,21 @@ require 'irb/color' module IRB class ColorPrinter < ::PP - def self.pp(obj, out = $>, width = 79) - q = ColorPrinter.new(out, width) - q.guard_inspect_key {q.pp obj} - q.flush - out << "\n" + 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 - + 79 + end end def text(str, width = nil) diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb index 1b683d88e5..90dcd70bd0 100644 --- a/lib/irb/ext/loader.rb +++ b/lib/irb/ext/loader.rb @@ -31,8 +31,31 @@ module IRB # :nodoc: load_file(path, priv) end + if File.respond_to?(:absolute_path?) + def absolute_path?(path) + File.absolute_path?(path) + end + else + separator = + if File::ALT_SEPARATOR + File::SEPARATOR + else + "[#{Regexp.quote(File::SEPARATOR + File::ALT_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 /^#{Regexp.quote(File::Separator)}/ =~ fn + if absolute_path?(fn) return fn if File.exist?(fn) return nil end @@ -50,16 +73,18 @@ module IRB # :nodoc: # 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 + 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 - rescue LoadAbort - print "load abort!!\n" + else + begin + irb.eval_input + rescue LoadAbort + print "load abort!!\n" + end end end end @@ -79,16 +104,18 @@ module IRB # :nodoc: 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 - if back_io.kind_of?(FileInputMethod) - irb.eval_input - else - begin + 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 - rescue LoadAbort - print "load abort!!\n" + else + begin + irb.eval_input + rescue LoadAbort + print "load abort!!\n" + end end end end diff --git a/lib/irb/init.rb b/lib/irb/init.rb index 8428a4278f..78ef2fa3c1 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -146,7 +146,7 @@ module IRB # :nodoc: @CONF[:AT_EXIT] = [] end - def IRB.set_measure_callback(type = nil, arg = nil) + def IRB.set_measure_callback(type = nil, arg = nil, &block) added = nil if type type_sym = type.upcase.to_sym @@ -155,6 +155,16 @@ module IRB # :nodoc: 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 diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index 61540a106f..e223672985 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -124,10 +124,22 @@ module IRB # 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 = 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 @@ -137,7 +149,7 @@ module IRB # # See IO#eof? for more information. def eof? - @io.eof? + @io.closed? || @io.eof? end # Reads the next line from this input method. @@ -150,13 +162,17 @@ module IRB # The external encoding for standard input. def encoding - @io.external_encoding + @external_encoding end # For debug message def inspect 'FileInputMethod' end + + def close + @io.close + end end begin diff --git a/lib/irb/irb.gemspec b/lib/irb/irb.gemspec index 9d889dfbf6..9842b4bce1 100644 --- a/lib/irb/irb.gemspec +++ b/lib/irb/irb.gemspec @@ -32,6 +32,7 @@ Gem::Specification.new do |spec| "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", diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index 35af148d02..ce94797dad 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -223,7 +223,10 @@ class RubyLex throw :TERM_INPUT if @line == '' else @line_no += l.count("\n") - next if l == "\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 @@ -233,7 +236,7 @@ class RubyLex @line.force_encoding(@io.encoding) yield @line, @exp_line_no end - break if @io.eof? + raise TerminateLineInput if @io.eof? @line = '' @exp_line_no = @line_no @@ -424,14 +427,30 @@ class RubyLex indent end + def is_method_calling?(tokens, index) + tk = tokens[index] + if tk[3].anybits?(Ripper::EXPR_CMDARG) and tk[1] == :on_ident + # The target method call to pass the block with "do". + return true + elsif tk[3].anybits?(Ripper::EXPR_ARG) and tk[1] == :on_ident + non_sp_index = tokens[0..(index - 1)].rindex{ |t| t[1] != :on_sp } + if non_sp_index + prev_tk = tokens[non_sp_index] + if prev_tk[3].anybits?(Ripper::EXPR_DOT) and prev_tk[1] == :on_period + # The target method call with receiver to pass the block with "do". + return true + end + end + end + false + end + def take_corresponding_syntax_to_kw_do(tokens, index) syntax_of_do = nil # Finding a syntax correnponding to "do". index.downto(0) do |i| tk = tokens[i] # In "continue", the token isn't the corresponding syntax to "do". - #is_continue = process_continue(@tokens[0..(i - 1)]) - # continue ではなく、直前に (:on_ignored_nl|:on_nl|:on_comment):on_sp* みたいなのがあるかどうかを調べる non_sp_index = tokens[0..(i - 1)].rindex{ |t| t[1] != :on_sp } first_in_fomula = false if non_sp_index.nil? @@ -439,8 +458,7 @@ class RubyLex elsif [:on_ignored_nl, :on_nl, :on_comment].include?(tokens[non_sp_index][1]) first_in_fomula = true end - if tk[3].anybits?(Ripper::EXPR_CMDARG) and tk[1] == :on_ident - # The target method call to pass the block with "do". + if is_method_calling?(tokens, i) syntax_of_do = :method_calling break if first_in_fomula elsif tk[1] == :on_kw && %w{while until for}.include?(tk[2]) @@ -458,6 +476,34 @@ class RubyLex syntax_of_do end + def is_the_in_correspond_to_a_for(tokens, index) + syntax_of_in = nil + # Finding a syntax correnponding 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[1] != :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][1]) + first_in_fomula = true + end + if tk[1] == :on_kw && tk[2] == '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 + def check_newline_depth_difference depth_difference = 0 open_brace_on_line = 0 @@ -513,8 +559,12 @@ class RubyLex unless t[3].allbits?(Ripper::EXPR_LABEL) depth_difference += 1 end - when 'else', 'elsif', 'ensure', 'when', 'in' + 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 diff --git a/lib/irb/version.rb b/lib/irb/version.rb index 51b55766a5..a715293b34 100644 --- a/lib/irb/version.rb +++ b/lib/irb/version.rb @@ -11,7 +11,7 @@ # module IRB # :nodoc: - VERSION = "1.3.2" + VERSION = "1.3.3" @RELEASE_VERSION = VERSION - @LAST_UPDATE_DATE = "2021-01-18" + @LAST_UPDATE_DATE = "2021-02-07" end diff --git a/lib/reline.rb b/lib/reline.rb index 52a4b3c78f..81ea9f9b58 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -243,6 +243,7 @@ module Reline loop do prev_pasting_state = Reline::IOGate.in_pasting? read_io(config.keyseq_timeout) { |inputs| + line_editor.set_pasting_state(Reline::IOGate.in_pasting?) inputs.each { |c| line_editor.input_key(c) line_editor.rerender @@ -253,6 +254,7 @@ module Reline end } if prev_pasting_state == true and not Reline::IOGate.in_pasting? and not line_editor.finished? + line_editor.set_pasting_state(false) prev_pasting_state = false line_editor.rerender_all end diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 92ea42fffb..557b5aa737 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -58,13 +58,17 @@ class Reline::LineEditor reset_variables(encoding: encoding) end + def set_pasting_state(in_pasting) + @in_pasting = in_pasting + end + def simplified_rendering? if finished? false elsif @just_cursor_moving and not @rerender_all true else - not @rerender_all and not finished? and Reline::IOGate.in_pasting? + not @rerender_all and not finished? and @in_pasting end end @@ -146,6 +150,13 @@ class Reline::LineEditor @screen_height = @screen_size.first reset_variables(prompt, encoding: encoding) @old_trap = Signal.trap('SIGINT') { + if @scroll_partial_screen + move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1) + else + move_cursor_down(@highest_in_all - @line_index - 1) + end + Reline::IOGate.move_cursor_column(0) + scroll_down(1) @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT" raise Interrupt } @@ -227,6 +238,8 @@ class Reline::LineEditor @scroll_partial_screen = nil @prev_mode_string = nil @drop_terminate_spaces = false + @in_pasting = false + @auto_indent_proc = nil reset_line end @@ -375,11 +388,29 @@ class Reline::LineEditor @cleared = false return end + if @is_multiline and finished? and @scroll_partial_screen + # Re-output all code higher than the screen when finished. + Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen) + Reline::IOGate.move_cursor_column(0) + @scroll_partial_screen = nil + prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) + if @previous_line_index + new_lines = whole_lines(index: @previous_line_index, line: @line) + else + new_lines = whole_lines + end + modify_lines(new_lines).each_with_index do |line, index| + @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n" + Reline::IOGate.erase_after_cursor + end + @output.flush + return + end new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line)) # FIXME: end of logical line sometimes breaks rendered = false if @add_newline_to_end_of_buffer - rerender_added_newline + rerender_added_newline(prompt, prompt_width) @add_newline_to_end_of_buffer = false else if @just_cursor_moving and not @rerender_all @@ -397,20 +428,32 @@ class Reline::LineEditor else end end - line = modify_lines(whole_lines)[@line_index] if @is_multiline - prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) if finished? # Always rerender on finish because output_modifier_proc may return a different output. + if @previous_line_index + new_lines = whole_lines(index: @previous_line_index, line: @line) + else + new_lines = whole_lines + end + line = modify_lines(new_lines)[@line_index] + prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt) render_partial(prompt, prompt_width, line, @first_line_started_from) + move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1) scroll_down(1) Reline::IOGate.move_cursor_column(0) Reline::IOGate.erase_after_cursor elsif not rendered - render_partial(prompt, prompt_width, line, @first_line_started_from) + unless @in_pasting + line = modify_lines(whole_lines)[@line_index] + prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) + render_partial(prompt, prompt_width, line, @first_line_started_from) + end end @buffer_of_lines[@line_index] = @line + @rest_height = 0 if @scroll_partial_screen else + line = modify_lines(whole_lines)[@line_index] render_partial(prompt, prompt_width, line, 0) if finished? scroll_down(1) @@ -453,13 +496,13 @@ class Reline::LineEditor end end - private def rerender_added_newline + private def rerender_added_newline(prompt, prompt_width) scroll_down(1) - new_lines = whole_lines(index: @previous_line_index, line: @line) - prompt, prompt_width, = check_multiline_prompt(new_lines, prompt) @buffer_of_lines[@previous_line_index] = @line @line = @buffer_of_lines[@line_index] - render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false) + unless @in_pasting + render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false) + end @cursor = @cursor_max = calculate_width(@line) @byte_pointer = @line.bytesize @highest_in_all += @highest_in_this @@ -568,7 +611,13 @@ class Reline::LineEditor new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt) end new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1 - if back > old_highest_in_all + calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from) + if @scroll_partial_screen + move_cursor_up(@first_line_started_from + @started_from) + scroll_down(@screen_height - 1) + move_cursor_up(@screen_height) + Reline::IOGate.move_cursor_column(0) + elsif back > old_highest_in_all scroll_down(back - 1) move_cursor_up(back - 1) elsif back < old_highest_in_all @@ -580,7 +629,6 @@ class Reline::LineEditor end move_cursor_up(old_highest_in_all - 1) end - calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from) render_whole_lines(new_buffer, prompt_list || prompt, prompt_width) if @prompt_proc prompt = prompt_list[@line_index] @@ -666,8 +714,8 @@ class Reline::LineEditor @highest_in_this = height end move_cursor_up(@started_from) - cursor_up_from_last_line = height - 1 - @started_from @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 + cursor_up_from_last_line = height - 1 - @started_from end if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render) @output.write "\e[0m" # clear character decorations @@ -1082,7 +1130,7 @@ class Reline::LineEditor unless completion_occurs @completion_state = CompletionState::NORMAL end - if not Reline::IOGate.in_pasting? and @just_cursor_moving.nil? + if not @in_pasting and @just_cursor_moving.nil? if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line @just_cursor_moving = true elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line @@ -1286,7 +1334,11 @@ class Reline::LineEditor if @buffer_of_lines.size == 1 and @line.nil? nil else - whole_lines.join("\n") + if @previous_line_index + whole_lines(index: @previous_line_index, line: @line).join("\n") + else + whole_lines.join("\n") + end end end @@ -1332,14 +1384,14 @@ class Reline::LineEditor cursor_line = @line.byteslice(0, @byte_pointer) insert_new_line(cursor_line, next_line) @cursor = 0 - @check_new_auto_indent = true unless Reline::IOGate.in_pasting? + @check_new_auto_indent = true unless @in_pasting end end private def ed_unassigned(key) end # do nothing private def process_insert(force: false) - return if @continuous_insertion_buffer.empty? or (Reline::IOGate.in_pasting? and not force) + return if @continuous_insertion_buffer.empty? or (@in_pasting and not force) width = Reline::Unicode.calculate_width(@continuous_insertion_buffer) bytesize = @continuous_insertion_buffer.bytesize if @cursor == @cursor_max @@ -1374,7 +1426,7 @@ class Reline::LineEditor str = key.chr bytesize = 1 end - if Reline::IOGate.in_pasting? + if @in_pasting @continuous_insertion_buffer << str return elsif not @continuous_insertion_buffer.empty? @@ -2424,11 +2476,23 @@ class Reline::LineEditor private def vi_histedit(key) path = Tempfile.open { |fp| - fp.write @line + if @is_multiline + fp.write whole_lines.join("\n") + else + fp.write @line + end fp.path } system("#{ENV['EDITOR']} #{path}") - @line = File.read(path) + if @is_multiline + @buffer_of_lines = File.read(path).split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = 0 + @line = @buffer_of_lines[@line_index] + @rerender_all = true + else + @line = File.read(path) + end finish end diff --git a/lib/reline/version.rb b/lib/reline/version.rb index 9241aef5a9..5b20f6f3e7 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.2.2' + VERSION = '0.2.3' end diff --git a/test/irb/test_cmd.rb b/test/irb/test_cmd.rb index b2246dfff5..7219473e4c 100644 --- a/test/irb/test_cmd.rb +++ b/test/irb/test_cmd.rb @@ -275,5 +275,98 @@ module TestIRB assert_empty err assert_match(/\A=> 3\nCUSTOM is added\.\n=> nil\ncustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) end + + def test_measure_with_proc + IRB.init_config(nil) + IRB.conf[:PROMPT] = { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ', + PROMPT_N: '> ' + } + } + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :DEFAULT + IRB.conf[:MEASURE] = false + input = TestInputMethod.new([ + "3\n", + "measure { |context, code, line_no, &block|\n", + " result = block.()\n", + " puts 'aaa' if IRB.conf[:MEASURE]\n", + " result\n", + "}\n", + "3\n", + "measure { |context, code, line_no, &block|\n", + " result = block.()\n", + " puts 'bbb' if IRB.conf[:MEASURE]\n", + " result\n", + "}\n", + "3\n", + "measure :off\n", + "3\n", + ]) + c = Class.new(Object) + irb = IRB::Irb.new(IRB::WorkSpace.new(c.new), input) + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(/\A=> 3\nBLOCK is added\.\n=> nil\naaa\n=> 3\nBLOCK is added.\naaa\n=> nil\nbbb\n=> 3\n=> nil\n=> 3\n/, out) + assert_empty(c.class_variables) + end + + def test_irb_source + IRB.init_config(nil) + File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") + input = TestInputMethod.new([ + "a = 'bug17564'\n", + "a\n", + "irb_source '#{@tmpdir}/a.rb'\n", + "a\n", + ]) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + irb = IRB::Irb.new(IRB::WorkSpace.new(self), input) + IRB.conf[:MAIN_CONTEXT] = irb.context + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_pattern_list([ + /=> "bug17564"\n/, + /=> "bug17564"\n/, + / => "hi"\n/, + / => nil\n/, + /=> "hi"\n/, + ], out) + end + + def test_irb_load + IRB.init_config(nil) + File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") + input = TestInputMethod.new([ + "a = 'bug17564'\n", + "a\n", + "irb_load '#{@tmpdir}/a.rb'\n", + "a\n", + ]) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + irb = IRB::Irb.new(IRB::WorkSpace.new(self), input) + IRB.conf[:MAIN_CONTEXT] = irb.context + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_pattern_list([ + /=> "bug17564"\n/, + /=> "bug17564"\n/, + / => "hi"\n/, + / => nil\n/, + /=> "bug17564"\n/, + ], out) + end end end diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index d035e443a8..9976008124 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -1,7 +1,6 @@ # frozen_string_literal: false require 'test/unit' require 'irb/color' -require 'irb/color_printer' require 'rubygems' require 'stringio' @@ -153,23 +152,6 @@ module TestIRB end end - IRBTestColorPrinter = Struct.new(:a) - - def test_color_printer - unless ripper_lexer_scan_supported? - skip 'Ripper::Lexer#scan is supported in Ruby 2.7+' - end - { - 1 => "#{BLUE}#{BOLD}1#{CLEAR}\n", - IRBTestColorPrinter.new('test') => "#{GREEN}##{CLEAR}\n", - Ripper::Lexer.new('1').scan => "[#{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", - }.each do |object, result| - actual = with_term { IRB::ColorPrinter.pp(object, '') } - assert_equal(result, actual, "Case: IRB::ColorPrinter.pp(#{object.inspect}, '')") - end - end - def test_inspect_colorable { 1 => true, @@ -202,10 +184,6 @@ module TestIRB Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') end - def ripper_lexer_scan_supported? - Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') - end - def with_term stdout = $stdout io = StringIO.new diff --git a/test/irb/test_color_printer.rb b/test/irb/test_color_printer.rb new file mode 100644 index 0000000000..1b28837658 --- /dev/null +++ b/test/irb/test_color_printer.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: false +require 'test/unit' +require 'irb/color_printer' +require 'rubygems' +require 'stringio' + +module TestIRB + class TestColorPrinter < Test::Unit::TestCase + CLEAR = "\e[0m" + BOLD = "\e[1m" + RED = "\e[31m" + GREEN = "\e[32m" + BLUE = "\e[34m" + CYAN = "\e[36m" + + def setup + @get_screen_size = Reline.method(:get_screen_size) + Reline.instance_eval { undef :get_screen_size } + def Reline.get_screen_size + [36, 80] + end + end + + def teardown + Reline.instance_eval { undef :get_screen_size } + Reline.define_singleton_method(:get_screen_size, @get_screen_size) + end + + IRBTestColorPrinter = Struct.new(:a) + + def test_color_printer + unless ripper_lexer_scan_supported? + skip 'Ripper::Lexer#scan is supported in Ruby 2.7+' + end + { + 1 => "#{BLUE}#{BOLD}1#{CLEAR}\n", + IRBTestColorPrinter.new('test') => "#{GREEN}##{CLEAR}\n", + Ripper::Lexer.new('1').scan => "[#{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", + }.each do |object, result| + actual = with_term { IRB::ColorPrinter.pp(object, '') } + assert_equal(result, actual, "Case: IRB::ColorPrinter.pp(#{object.inspect}, '')") + end + end + + private + + def ripper_lexer_scan_supported? + Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') + end + + def with_term + stdout = $stdout + io = StringIO.new + def io.tty?; true; end + $stdout = io + + env = ENV.to_h.dup + ENV['TERM'] = 'xterm-256color' + + yield + ensure + $stdout = stdout + ENV.replace(env) if env + end + end +end diff --git a/test/irb/test_context.rb b/test/irb/test_context.rb index f3d0626caa..71e8ad1c0d 100644 --- a/test/irb/test_context.rb +++ b/test/irb/test_context.rb @@ -42,6 +42,17 @@ module TestIRB IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(Object.new) @context = IRB::Context.new(nil, workspace, TestInputMethod.new) + + @get_screen_size = Reline.method(:get_screen_size) + Reline.instance_eval { undef :get_screen_size } + def Reline.get_screen_size + [36, 80] + end + end + + def teardown + Reline.instance_eval { undef :get_screen_size } + Reline.define_singleton_method(:get_screen_size, @get_screen_size) end def test_last_value @@ -447,7 +458,7 @@ module TestIRB irb.eval_input end assert_empty err - if '2.5.0' <= RUBY_VERSION && RUBY_VERSION < '3.0.0' + if '2.5.0' <= RUBY_VERSION && RUBY_VERSION < '3.0.0' && STDOUT.tty? expected = [ :*, /Traceback \(most recent call last\):\n/, :*, /\t 2: from \(irb\):1:in `
'\n/, @@ -477,7 +488,7 @@ module TestIRB irb.eval_input end assert_empty err - if '2.5.0' <= RUBY_VERSION && RUBY_VERSION < '3.0.0' + if '2.5.0' <= RUBY_VERSION && RUBY_VERSION < '3.0.0' && STDOUT.tty? expected = [ :*, /Traceback \(most recent call last\):\n/, :*, /\t 2: from \(irb\):1:in `
'\n/, @@ -513,7 +524,7 @@ module TestIRB irb.eval_input end assert_empty err - if '2.5.0' <= RUBY_VERSION && RUBY_VERSION < '3.0.0' + if '2.5.0' <= RUBY_VERSION && RUBY_VERSION < '3.0.0' && STDOUT.tty? expected = [ :*, /Traceback \(most recent call last\):\n/, :*, /\t... 5 levels...\n/, @@ -561,5 +572,26 @@ module TestIRB ensure $VERBOSE = verbose end + + def test_lineno + input = TestInputMethod.new([ + "\n", + "__LINE__\n", + "__LINE__\n", + "\n", + "\n", + "__LINE__\n", + ]) + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_pattern_list([ + :*, /\b2\n/, + :*, /\b3\n/, + :*, /\b6\n/, + ], out) + end end end diff --git a/test/irb/test_ruby_lex.rb b/test/irb/test_ruby_lex.rb index ed4944afc6..a45ca668b9 100644 --- a/test/irb/test_ruby_lex.rb +++ b/test/irb/test_ruby_lex.rb @@ -333,6 +333,102 @@ module TestIRB end end + def test_corresponding_syntax_to_keyword_for + input_with_correct_indents = [ + Row.new(%q(for i in [1]), nil, 2, 1), + Row.new(%q( puts i), nil, 2, 1), + Row.new(%q(end), 0, 0, 0), + ] + + lines = [] + input_with_correct_indents.each do |row| + lines << row.content + assert_indenting(lines, row.current_line_spaces, false) + assert_indenting(lines, row.new_line_spaces, true) + assert_nesting_level(lines, row.nesting_level) + end + end + + def test_corresponding_syntax_to_keyword_for_with_do + input_with_correct_indents = [ + Row.new(%q(for i in [1] do), nil, 2, 1), + Row.new(%q( puts i), nil, 2, 1), + Row.new(%q(end), 0, 0, 0), + ] + + lines = [] + input_with_correct_indents.each do |row| + lines << row.content + assert_indenting(lines, row.current_line_spaces, false) + assert_indenting(lines, row.new_line_spaces, true) + assert_nesting_level(lines, row.nesting_level) + end + end + + def test_bracket_corresponding_to_times + input_with_correct_indents = [ + Row.new(%q(3.times { |i|), nil, 2, 1), + Row.new(%q( puts i), nil, 2, 1), + Row.new(%q(}), 0, 0, 0), + ] + + lines = [] + input_with_correct_indents.each do |row| + lines << row.content + assert_indenting(lines, row.current_line_spaces, false) + assert_indenting(lines, row.new_line_spaces, true) + assert_nesting_level(lines, row.nesting_level) + end + end + + def test_do_corresponding_to_times + input_with_correct_indents = [ + Row.new(%q(3.times do |i|), nil, 2, 1), + #Row.new(%q( puts i), nil, 2, 1), + #Row.new(%q(end), 0, 0, 0), + ] + + lines = [] + input_with_correct_indents.each do |row| + lines << row.content + assert_indenting(lines, row.current_line_spaces, false) + assert_indenting(lines, row.new_line_spaces, true) + assert_nesting_level(lines, row.nesting_level) + end + end + + def test_bracket_corresponding_to_loop + input_with_correct_indents = [ + Row.new(%q(loop {), nil, 2, 1), + Row.new(%q( 3), nil, 2, 1), + Row.new(%q(}), 0, 0, 0), + ] + + lines = [] + input_with_correct_indents.each do |row| + lines << row.content + assert_indenting(lines, row.current_line_spaces, false) + assert_indenting(lines, row.new_line_spaces, true) + assert_nesting_level(lines, row.nesting_level) + end + end + + def test_do_corresponding_to_loop + input_with_correct_indents = [ + Row.new(%q(loop do), nil, 2, 1), + Row.new(%q( 3), nil, 2, 1), + Row.new(%q(end), 0, 0, 0), + ] + + lines = [] + input_with_correct_indents.each do |row| + lines << row.content + assert_indenting(lines, row.current_line_spaces, false) + assert_indenting(lines, row.new_line_spaces, true) + assert_nesting_level(lines, row.nesting_level) + end + end + def test_heredoc_with_indent input_with_correct_indents = [ Row.new(%q(<<~Q), nil, 0, 0), diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index b583f8ddac..0ccc331efd 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -678,6 +678,34 @@ begin EOC end + def test_autowrap_in_the_middle_of_a_line + start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.') + write("def abcdefg; end\C-b\C-b\C-b\C-b\C-b") + %w{h i}.each do |c| + write(c) + end + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> def abcdefgh + i; end + EOC + end + + def test_terminate_in_the_middle_of_lines + start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.') + write("def hoge\n 1\n 2\n 3\n 4\nend\n") + write("\C-p\C-p\C-p\C-e\n") + close + assert_screen(<<~EOC) + prompt> 3 + prompt> 4 + prompt> end + => :hoge + prompt> + EOC + end + private def write_inputrc(content) File.open(@inputrc_file, 'w') do |f| f.write content