From a6fe45f5ba8ab991c48e4cf38d65588685e32d53 Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Tue, 3 Dec 2024 01:44:44 +0900 Subject: [PATCH] Implement buffered output to Reline::ANSI (#790) Minimize the call of STDOUT.write This will improve rendering performance especially when there is a busy thread `Thread.new{loop{}}` --- lib/reline.rb | 5 +--- lib/reline/io/ansi.rb | 49 ++++++++++++++++++++------------- lib/reline/io/dumb.rb | 11 ++++++++ lib/reline/io/windows.rb | 11 ++++++++ lib/reline/line_editor.rb | 25 +++++++++-------- test/reline/test_line_editor.rb | 5 +++- test/reline/test_macro.rb | 2 +- 7 files changed, 72 insertions(+), 36 deletions(-) diff --git a/lib/reline.rb b/lib/reline.rb index 867d2e03dd..9af3a5c332 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -181,9 +181,7 @@ def input=(val) def output=(val) raise TypeError unless val.respond_to?(:write) or val.nil? @output = val - if io_gate.respond_to?(:output=) - io_gate.output = val - end + io_gate.output = val end def vi_editing_mode @@ -317,7 +315,6 @@ def readline(prompt = '', add_hist = false) else line_editor.multiline_off end - line_editor.output = output line_editor.completion_proc = completion_proc line_editor.completion_append_character = completion_append_character line_editor.output_modifier_proc = output_modifier_proc diff --git a/lib/reline/io/ansi.rb b/lib/reline/io/ansi.rb index b351952a82..500eb37a13 100644 --- a/lib/reline/io/ansi.rb +++ b/lib/reline/io/ansi.rb @@ -29,10 +29,13 @@ class Reline::ANSI < Reline::IO 'H' => [:ed_move_to_beg, {}], } + attr_writer :input, :output + def initialize @input = STDIN @output = STDOUT @buf = [] + @output_buffer = nil @old_winch_handler = nil end @@ -114,14 +117,6 @@ def set_default_key_bindings_comprehensive_list(config) end end - def input=(val) - @input = val - end - - def output=(val) - @output = val - end - def with_raw_input if @input.tty? @input.raw(intr: true) { yield } @@ -238,13 +233,29 @@ def both_tty? @input.tty? && @output.tty? end + def write(string) + if @output_buffer + @output_buffer << string + else + @output.write(string) + end + end + + def buffered_output + @output_buffer = +'' + yield + @output.write(@output_buffer) + ensure + @output_buffer = nil + end + def move_cursor_column(x) - @output.write "\e[#{x + 1}G" + write "\e[#{x + 1}G" end def move_cursor_up(x) if x > 0 - @output.write "\e[#{x}A" + write "\e[#{x}A" elsif x < 0 move_cursor_down(-x) end @@ -252,22 +263,22 @@ def move_cursor_up(x) def move_cursor_down(x) if x > 0 - @output.write "\e[#{x}B" + write "\e[#{x}B" elsif x < 0 move_cursor_up(-x) end end def hide_cursor - @output.write "\e[?25l" + write "\e[?25l" end def show_cursor - @output.write "\e[?25h" + write "\e[?25h" end def erase_after_cursor - @output.write "\e[K" + write "\e[K" end # This only works when the cursor is at the bottom of the scroll range @@ -275,12 +286,12 @@ def erase_after_cursor def scroll_down(x) return if x.zero? # We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576 - @output.write "\n" * x + write "\n" * x end def clear_screen - @output.write "\e[2J" - @output.write "\e[1;1H" + write "\e[2J" + write "\e[1;1H" end def set_winch_handler(&handler) @@ -300,14 +311,14 @@ def set_winch_handler(&handler) def prep # Enable bracketed paste - @output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty? + write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty? retrieve_keybuffer nil end def deprep(otio) # Disable bracketed paste - @output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty? + write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty? Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler Signal.trap('CONT', @old_cont_handler) if @old_cont_handler end diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb index e58ba0f6d8..0c04c755d2 100644 --- a/lib/reline/io/dumb.rb +++ b/lib/reline/io/dumb.rb @@ -3,8 +3,11 @@ class Reline::Dumb < Reline::IO RESET_COLOR = '' # Do not send color reset sequence + attr_writer :output + def initialize(encoding: nil) @input = STDIN + @output = STDOUT @buf = [] @pasting = false @encoding = encoding @@ -39,6 +42,14 @@ def with_raw_input yield end + def write(string) + @output.write(string) + end + + def buffered_output + yield + end + def getc(_timeout_second) unless @buf.empty? return @buf.shift diff --git a/lib/reline/io/windows.rb b/lib/reline/io/windows.rb index 29eab28073..058d65c473 100644 --- a/lib/reline/io/windows.rb +++ b/lib/reline/io/windows.rb @@ -1,6 +1,9 @@ require 'fiddle/import' class Reline::Windows < Reline::IO + + attr_writer :output + def initialize @input_buf = [] @output_buf = [] @@ -308,6 +311,14 @@ def with_raw_input yield end + def write(string) + @output.write(string) + end + + def buffered_output + yield + end + def getc(_timeout_second) check_input_event @output_buf.shift diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 5cbef1380b..c5658137ee 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -13,7 +13,6 @@ class Reline::LineEditor attr_accessor :prompt_proc attr_accessor :auto_indent_proc attr_accessor :dig_perfect_match_proc - attr_writer :output VI_MOTIONS = %i{ ed_prev_char @@ -414,7 +413,7 @@ def render_line_differential(old_items, new_items) # do nothing elsif level == :blank Reline::IOGate.move_cursor_column base_x - @output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}" + Reline::IOGate.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}" else x, w, content = new_items[level] cover_begin = base_x != 0 && new_levels[base_x - 1] == level @@ -424,7 +423,7 @@ def render_line_differential(old_items, new_items) content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true) end Reline::IOGate.move_cursor_column x + pos - @output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}" + Reline::IOGate.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}" end base_x += width end @@ -460,19 +459,21 @@ def update_dialogs(key = nil) end def render_finished - render_differential([], 0, 0) - lines = @buffer_of_lines.size.times.map do |i| - line = Reline::Unicode.strip_non_printing_start_end(prompt_list[i]) + modified_lines[i] - wrapped_lines = split_line_by_width(line, screen_width) - wrapped_lines.last.empty? ? "#{line} " : line + Reline::IOGate.buffered_output do + render_differential([], 0, 0) + lines = @buffer_of_lines.size.times.map do |i| + line = Reline::Unicode.strip_non_printing_start_end(prompt_list[i]) + modified_lines[i] + wrapped_lines = split_line_by_width(line, screen_width) + wrapped_lines.last.empty? ? "#{line} " : line + end + Reline::IOGate.write lines.map { |l| "#{l}\r\n" }.join end - @output.puts lines.map { |l| "#{l}\r\n" }.join end def print_nomultiline_prompt Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win? # Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence. - @output.write Reline::Unicode.strip_non_printing_start_end(@prompt) if @prompt && !@is_multiline + Reline::IOGate.write Reline::Unicode.strip_non_printing_start_end(@prompt) if @prompt && !@is_multiline ensure Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win? end @@ -503,7 +504,9 @@ def render end end - render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top + Reline::IOGate.buffered_output do + render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top + end end # Reflects lines to be rendered and new cursor position to the screen diff --git a/test/reline/test_line_editor.rb b/test/reline/test_line_editor.rb index 256ce99241..85a814a799 100644 --- a/test/reline/test_line_editor.rb +++ b/test/reline/test_line_editor.rb @@ -61,6 +61,10 @@ def test_retrieve_completion_quote class RenderLineDifferentialTest < Reline::TestCase class TestIO < Reline::IO + def write(string) + @output << string + end + def move_cursor_column(col) @output << "[COL_#{col}]" end @@ -76,7 +80,6 @@ def setup @original_iogate = Reline::IOGate @output = StringIO.new @line_editor.instance_variable_set(:@screen_size, [24, 80]) - @line_editor.instance_variable_set(:@output, @output) Reline.send(:remove_const, :IOGate) Reline.const_set(:IOGate, TestIO.new) Reline::IOGate.instance_variable_set(:@output, @output) diff --git a/test/reline/test_macro.rb b/test/reline/test_macro.rb index 319d7b76b1..bfee280c72 100644 --- a/test/reline/test_macro.rb +++ b/test/reline/test_macro.rb @@ -6,7 +6,7 @@ def setup @config = Reline::Config.new @encoding = Reline.core.encoding @line_editor = Reline::LineEditor.new(@config) - @output = @line_editor.output = File.open(IO::NULL, "w") + @output = Reline::IOGate.output = File.open(IO::NULL, "w") end def teardown