Skip to content

Commit

Permalink
Implement buffered output to Reline::ANSI (#790)
Browse files Browse the repository at this point in the history
Minimize the call of STDOUT.write
This will improve rendering performance especially when there is a busy thread `Thread.new{loop{}}`
  • Loading branch information
tompng authored Dec 2, 2024
1 parent 7d44770 commit a6fe45f
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 36 deletions.
5 changes: 1 addition & 4 deletions lib/reline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
49 changes: 30 additions & 19 deletions lib/reline/io/ansi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -238,49 +233,65 @@ 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
end

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
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
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)
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/reline/io/dumb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/reline/io/windows.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
require 'fiddle/import'

class Reline::Windows < Reline::IO

attr_writer :output

def initialize
@input_buf = []
@output_buf = []
Expand Down Expand Up @@ -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
Expand Down
25 changes: 14 additions & 11 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion test/reline/test_line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/reline/test_macro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a6fe45f

Please sign in to comment.