Skip to content

Commit a6fe45f

Browse files
authored
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{}}`
1 parent 7d44770 commit a6fe45f

File tree

7 files changed

+72
-36
lines changed

7 files changed

+72
-36
lines changed

lib/reline.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,7 @@ def input=(val)
181181
def output=(val)
182182
raise TypeError unless val.respond_to?(:write) or val.nil?
183183
@output = val
184-
if io_gate.respond_to?(:output=)
185-
io_gate.output = val
186-
end
184+
io_gate.output = val
187185
end
188186

189187
def vi_editing_mode
@@ -317,7 +315,6 @@ def readline(prompt = '', add_hist = false)
317315
else
318316
line_editor.multiline_off
319317
end
320-
line_editor.output = output
321318
line_editor.completion_proc = completion_proc
322319
line_editor.completion_append_character = completion_append_character
323320
line_editor.output_modifier_proc = output_modifier_proc

lib/reline/io/ansi.rb

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ class Reline::ANSI < Reline::IO
2929
'H' => [:ed_move_to_beg, {}],
3030
}
3131

32+
attr_writer :input, :output
33+
3234
def initialize
3335
@input = STDIN
3436
@output = STDOUT
3537
@buf = []
38+
@output_buffer = nil
3639
@old_winch_handler = nil
3740
end
3841

@@ -114,14 +117,6 @@ def set_default_key_bindings_comprehensive_list(config)
114117
end
115118
end
116119

117-
def input=(val)
118-
@input = val
119-
end
120-
121-
def output=(val)
122-
@output = val
123-
end
124-
125120
def with_raw_input
126121
if @input.tty?
127122
@input.raw(intr: true) { yield }
@@ -238,49 +233,65 @@ def both_tty?
238233
@input.tty? && @output.tty?
239234
end
240235

236+
def write(string)
237+
if @output_buffer
238+
@output_buffer << string
239+
else
240+
@output.write(string)
241+
end
242+
end
243+
244+
def buffered_output
245+
@output_buffer = +''
246+
yield
247+
@output.write(@output_buffer)
248+
ensure
249+
@output_buffer = nil
250+
end
251+
241252
def move_cursor_column(x)
242-
@output.write "\e[#{x + 1}G"
253+
write "\e[#{x + 1}G"
243254
end
244255

245256
def move_cursor_up(x)
246257
if x > 0
247-
@output.write "\e[#{x}A"
258+
write "\e[#{x}A"
248259
elsif x < 0
249260
move_cursor_down(-x)
250261
end
251262
end
252263

253264
def move_cursor_down(x)
254265
if x > 0
255-
@output.write "\e[#{x}B"
266+
write "\e[#{x}B"
256267
elsif x < 0
257268
move_cursor_up(-x)
258269
end
259270
end
260271

261272
def hide_cursor
262-
@output.write "\e[?25l"
273+
write "\e[?25l"
263274
end
264275

265276
def show_cursor
266-
@output.write "\e[?25h"
277+
write "\e[?25h"
267278
end
268279

269280
def erase_after_cursor
270-
@output.write "\e[K"
281+
write "\e[K"
271282
end
272283

273284
# This only works when the cursor is at the bottom of the scroll range
274285
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
275286
def scroll_down(x)
276287
return if x.zero?
277288
# We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
278-
@output.write "\n" * x
289+
write "\n" * x
279290
end
280291

281292
def clear_screen
282-
@output.write "\e[2J"
283-
@output.write "\e[1;1H"
293+
write "\e[2J"
294+
write "\e[1;1H"
284295
end
285296

286297
def set_winch_handler(&handler)
@@ -300,14 +311,14 @@ def set_winch_handler(&handler)
300311

301312
def prep
302313
# Enable bracketed paste
303-
@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty?
314+
write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty?
304315
retrieve_keybuffer
305316
nil
306317
end
307318

308319
def deprep(otio)
309320
# Disable bracketed paste
310-
@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty?
321+
write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty?
311322
Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler
312323
Signal.trap('CONT', @old_cont_handler) if @old_cont_handler
313324
end

lib/reline/io/dumb.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
class Reline::Dumb < Reline::IO
44
RESET_COLOR = '' # Do not send color reset sequence
55

6+
attr_writer :output
7+
68
def initialize(encoding: nil)
79
@input = STDIN
10+
@output = STDOUT
811
@buf = []
912
@pasting = false
1013
@encoding = encoding
@@ -39,6 +42,14 @@ def with_raw_input
3942
yield
4043
end
4144

45+
def write(string)
46+
@output.write(string)
47+
end
48+
49+
def buffered_output
50+
yield
51+
end
52+
4253
def getc(_timeout_second)
4354
unless @buf.empty?
4455
return @buf.shift

lib/reline/io/windows.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
require 'fiddle/import'
22

33
class Reline::Windows < Reline::IO
4+
5+
attr_writer :output
6+
47
def initialize
58
@input_buf = []
69
@output_buf = []
@@ -308,6 +311,14 @@ def with_raw_input
308311
yield
309312
end
310313

314+
def write(string)
315+
@output.write(string)
316+
end
317+
318+
def buffered_output
319+
yield
320+
end
321+
311322
def getc(_timeout_second)
312323
check_input_event
313324
@output_buf.shift

lib/reline/line_editor.rb

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ class Reline::LineEditor
1313
attr_accessor :prompt_proc
1414
attr_accessor :auto_indent_proc
1515
attr_accessor :dig_perfect_match_proc
16-
attr_writer :output
1716

1817
VI_MOTIONS = %i{
1918
ed_prev_char
@@ -414,7 +413,7 @@ def render_line_differential(old_items, new_items)
414413
# do nothing
415414
elsif level == :blank
416415
Reline::IOGate.move_cursor_column base_x
417-
@output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
416+
Reline::IOGate.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
418417
else
419418
x, w, content = new_items[level]
420419
cover_begin = base_x != 0 && new_levels[base_x - 1] == level
@@ -424,7 +423,7 @@ def render_line_differential(old_items, new_items)
424423
content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
425424
end
426425
Reline::IOGate.move_cursor_column x + pos
427-
@output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
426+
Reline::IOGate.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
428427
end
429428
base_x += width
430429
end
@@ -460,19 +459,21 @@ def update_dialogs(key = nil)
460459
end
461460

462461
def render_finished
463-
render_differential([], 0, 0)
464-
lines = @buffer_of_lines.size.times.map do |i|
465-
line = Reline::Unicode.strip_non_printing_start_end(prompt_list[i]) + modified_lines[i]
466-
wrapped_lines = split_line_by_width(line, screen_width)
467-
wrapped_lines.last.empty? ? "#{line} " : line
462+
Reline::IOGate.buffered_output do
463+
render_differential([], 0, 0)
464+
lines = @buffer_of_lines.size.times.map do |i|
465+
line = Reline::Unicode.strip_non_printing_start_end(prompt_list[i]) + modified_lines[i]
466+
wrapped_lines = split_line_by_width(line, screen_width)
467+
wrapped_lines.last.empty? ? "#{line} " : line
468+
end
469+
Reline::IOGate.write lines.map { |l| "#{l}\r\n" }.join
468470
end
469-
@output.puts lines.map { |l| "#{l}\r\n" }.join
470471
end
471472

472473
def print_nomultiline_prompt
473474
Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
474475
# Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence.
475-
@output.write Reline::Unicode.strip_non_printing_start_end(@prompt) if @prompt && !@is_multiline
476+
Reline::IOGate.write Reline::Unicode.strip_non_printing_start_end(@prompt) if @prompt && !@is_multiline
476477
ensure
477478
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
478479
end
@@ -503,7 +504,9 @@ def render
503504
end
504505
end
505506

506-
render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top
507+
Reline::IOGate.buffered_output do
508+
render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top
509+
end
507510
end
508511

509512
# Reflects lines to be rendered and new cursor position to the screen

test/reline/test_line_editor.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def test_retrieve_completion_quote
6161

6262
class RenderLineDifferentialTest < Reline::TestCase
6363
class TestIO < Reline::IO
64+
def write(string)
65+
@output << string
66+
end
67+
6468
def move_cursor_column(col)
6569
@output << "[COL_#{col}]"
6670
end
@@ -76,7 +80,6 @@ def setup
7680
@original_iogate = Reline::IOGate
7781
@output = StringIO.new
7882
@line_editor.instance_variable_set(:@screen_size, [24, 80])
79-
@line_editor.instance_variable_set(:@output, @output)
8083
Reline.send(:remove_const, :IOGate)
8184
Reline.const_set(:IOGate, TestIO.new)
8285
Reline::IOGate.instance_variable_set(:@output, @output)

test/reline/test_macro.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def setup
66
@config = Reline::Config.new
77
@encoding = Reline.core.encoding
88
@line_editor = Reline::LineEditor.new(@config)
9-
@output = @line_editor.output = File.open(IO::NULL, "w")
9+
@output = Reline::IOGate.output = File.open(IO::NULL, "w")
1010
end
1111

1212
def teardown

0 commit comments

Comments
 (0)