Skip to content

Commit

Permalink
Implement buffered output to Reline::ANSI
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 committed Dec 2, 2024
1 parent 7d44770 commit 4bc11b1
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 28 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
42 changes: 25 additions & 17 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 = +''
@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,62 @@ def both_tty?
@input.tty? && @output.tty?
end

def write(string)
write_to_buffer(string)
end

def write_to_buffer(string)
@output_buffer << string
end

def flush
@output.write @output_buffer
@output_buffer.clear
end

def move_cursor_column(x)
@output.write "\e[#{x + 1}G"
write_to_buffer "\e[#{x + 1}G"
end

def move_cursor_up(x)
if x > 0
@output.write "\e[#{x}A"
write_to_buffer "\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_to_buffer "\e[#{x}B"
elsif x < 0
move_cursor_up(-x)
end
end

def hide_cursor
@output.write "\e[?25l"
write_to_buffer "\e[?25l"
end

def show_cursor
@output.write "\e[?25h"
write_to_buffer "\e[?25h"
end

def erase_after_cursor
@output.write "\e[K"
write_to_buffer "\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_to_buffer "\n" * x
end

def clear_screen
@output.write "\e[2J"
@output.write "\e[1;1H"
write_to_buffer "\e[2J"
write_to_buffer "\e[1;1H"
end

def set_winch_handler(&handler)
Expand Down
10 changes: 10 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,13 @@ def with_raw_input
yield
end

def write(string)
@output.write(string)
end

def flush
end

def getc(_timeout_second)
unless @buf.empty?
return @buf.shift
Expand Down
10 changes: 10 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,13 @@ def with_raw_input
yield
end

def write(string)
@output.write(string)
end

def flush
end

def getc(_timeout_second)
check_input_event
@output_buf.shift
Expand Down
17 changes: 12 additions & 5 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 @@ -178,6 +177,7 @@ def handle_signal
@resized = false
scroll_into_view
Reline::IOGate.move_cursor_up @rendered_screen.cursor_y
Reline::IOGate.flush
@rendered_screen.base_y = Reline::IOGate.cursor_pos.y
clear_rendered_screen_cache
render
Expand All @@ -192,6 +192,7 @@ def handle_signal
cursor_to_bottom_offset = @rendered_screen.lines.size - @rendered_screen.cursor_y
Reline::IOGate.scroll_down cursor_to_bottom_offset
Reline::IOGate.move_cursor_column 0
Reline::IOGate.flush
clear_rendered_screen_cache
case @old_trap
when 'DEFAULT', 'SYSTEM_DEFAULT'
Expand Down Expand Up @@ -414,7 +415,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 +425,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 @@ -466,13 +467,17 @@ def render_finished
wrapped_lines = split_line_by_width(line, screen_width)
wrapped_lines.last.empty? ? "#{line} " : line
end
@output.puts lines.map { |l| "#{l}\r\n" }.join
lines.each { |l| Reline::IOGate.write "#{l}\r\n" }
Reline::IOGate.flush
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
if @prompt && !@is_multiline
Reline::IOGate.write Reline::Unicode.strip_non_printing_start_end(@prompt)
Reline::IOGate.flush
end
ensure
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
end
Expand Down Expand Up @@ -540,6 +545,7 @@ def render
end
Reline::IOGate.move_cursor_column new_cursor_x
Reline::IOGate.move_cursor_down new_cursor_y - cursor_y
Reline::IOGate.flush
@rendered_screen.cursor_y = new_cursor_y
ensure
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
Expand Down Expand Up @@ -1868,6 +1874,7 @@ def finish

private def ed_clear_screen(key)
Reline::IOGate.clear_screen
Reline::IOGate.flush
@screen_size = Reline::IOGate.get_screen_size
@rendered_screen.base_y = 0
clear_rendered_screen_cache
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 4bc11b1

Please sign in to comment.