[ruby/reline] Support longer than screen height

https://github.com/ruby/reline/commit/e83a3de9ed
This commit is contained in:
aycabta 2020-12-13 11:30:05 +09:00
Родитель 9908177857
Коммит 2b8fa78176
2 изменённых файлов: 305 добавлений и 60 удалений

Просмотреть файл

@ -123,6 +123,7 @@ class Reline::LineEditor
def reset(prompt = '', encoding:) def reset(prompt = '', encoding:)
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
@screen_size = Reline::IOGate.get_screen_size @screen_size = Reline::IOGate.get_screen_size
@screen_height = @screen_size.first
reset_variables(prompt, encoding: encoding) reset_variables(prompt, encoding: encoding)
@old_trap = Signal.trap('SIGINT') { @old_trap = Signal.trap('SIGINT') {
@old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT" @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
@ -132,6 +133,7 @@ class Reline::LineEditor
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
old_screen_size = @screen_size old_screen_size = @screen_size
@screen_size = Reline::IOGate.get_screen_size @screen_size = Reline::IOGate.get_screen_size
@screen_height = @screen_size.first
if old_screen_size.last < @screen_size.last # columns increase if old_screen_size.last < @screen_size.last # columns increase
@rerender_all = true @rerender_all = true
rerender rerender
@ -202,6 +204,7 @@ class Reline::LineEditor
@prompt_cache_time = nil @prompt_cache_time = nil
@eof = false @eof = false
@continuous_insertion_buffer = String.new(encoding: @encoding) @continuous_insertion_buffer = String.new(encoding: @encoding)
@scroll_partial_screen = nil
reset_line reset_line
end end
@ -287,28 +290,28 @@ class Reline::LineEditor
end end
end end
private def calculate_nearest_cursor private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
@cursor_max = calculate_width(line) new_cursor_max = calculate_width(line_to_calc)
new_cursor = 0 new_cursor = 0
new_byte_pointer = 0 new_byte_pointer = 0
height = 1 height = 1
max_width = @screen_size.last max_width = @screen_size.last
if @config.editing_mode_is?(:vi_command) if @config.editing_mode_is?(:vi_command)
last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize) last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize)
if last_byte_size > 0 if last_byte_size > 0
last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size) last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size)
last_width = Reline::Unicode.get_mbchar_width(last_mbchar) last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
cursor_max = @cursor_max - last_width end_of_line_cursor = new_cursor_max - last_width
else else
cursor_max = @cursor_max end_of_line_cursor = new_cursor_max
end end
else else
cursor_max = @cursor_max end_of_line_cursor = new_cursor_max
end end
@line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc| line_to_calc.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
mbchar_width = Reline::Unicode.get_mbchar_width(gc) mbchar_width = Reline::Unicode.get_mbchar_width(gc)
now = new_cursor + mbchar_width now = new_cursor + mbchar_width
if now > cursor_max or now > @cursor if now > end_of_line_cursor or now > cursor
break break
end end
new_cursor += mbchar_width new_cursor += mbchar_width
@ -317,9 +320,15 @@ class Reline::LineEditor
end end
new_byte_pointer += gc.bytesize new_byte_pointer += gc.bytesize
end end
@started_from = height - 1 new_started_from = height - 1
@cursor = new_cursor if update
@byte_pointer = new_byte_pointer @cursor = new_cursor
@cursor_max = new_cursor_max
@started_from = new_started_from
@byte_pointer = new_byte_pointer
else
[new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
end
end end
def rerender_all def rerender_all
@ -349,33 +358,37 @@ class Reline::LineEditor
if @add_newline_to_end_of_buffer if @add_newline_to_end_of_buffer
rerender_added_newline rerender_added_newline
@add_newline_to_end_of_buffer = false @add_newline_to_end_of_buffer = false
elsif @just_cursor_moving and not @rerender_all else
just_move_cursor if @just_cursor_moving and not @rerender_all
@just_cursor_moving = false rendered = just_move_cursor
return @just_cursor_moving = false
elsif @previous_line_index or new_highest_in_this != @highest_in_this return
rerender_changed_current_line elsif @previous_line_index or new_highest_in_this != @highest_in_this
@previous_line_index = nil rerender_changed_current_line
rendered = true @previous_line_index = nil
elsif @rerender_all rendered = true
rerender_all_lines elsif @rerender_all
@rerender_all = false rerender_all_lines
rendered = true @rerender_all = false
rendered = true
else
end
end end
line = modify_lines(whole_lines)[@line_index] line = modify_lines(whole_lines)[@line_index]
if @is_multiline if @is_multiline
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
if finished? if finished?
# Always rerender on finish because output_modifier_proc may return a different output. # Always rerender on finish because output_modifier_proc may return a different output.
render_partial(prompt, prompt_width, line) render_partial(prompt, prompt_width, line, @first_line_started_from)
scroll_down(1) scroll_down(1)
Reline::IOGate.move_cursor_column(0) Reline::IOGate.move_cursor_column(0)
Reline::IOGate.erase_after_cursor Reline::IOGate.erase_after_cursor
elsif not rendered elsif not rendered
render_partial(prompt, prompt_width, line) render_partial(prompt, prompt_width, line, @first_line_started_from)
end end
@buffer_of_lines[@line_index] = @line
else else
render_partial(prompt, prompt_width, line) render_partial(prompt, prompt_width, line, 0)
if finished? if finished?
scroll_down(1) scroll_down(1)
Reline::IOGate.move_cursor_column(0) Reline::IOGate.move_cursor_column(0)
@ -384,13 +397,46 @@ class Reline::LineEditor
end end
end end
private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
if @screen_height < highest_in_all
old_scroll_partial_screen = @scroll_partial_screen
if cursor_y == 0
@scroll_partial_screen = 0
elsif cursor_y == (highest_in_all - 1)
@scroll_partial_screen = highest_in_all - @screen_height
else
if @scroll_partial_screen
if cursor_y <= @scroll_partial_screen
@scroll_partial_screen = cursor_y
elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
@scroll_partial_screen = cursor_y - (@screen_height - 1)
end
else
if cursor_y > (@screen_height - 1)
@scroll_partial_screen = cursor_y - (@screen_height - 1)
else
@scroll_partial_screen = 0
end
end
end
if @scroll_partial_screen != old_scroll_partial_screen
@rerender_all = true
end
else
if @scroll_partial_screen
@rerender_all = true
end
@scroll_partial_screen = nil
end
end
private def rerender_added_newline private def rerender_added_newline
scroll_down(1) scroll_down(1)
new_lines = whole_lines(index: @previous_line_index, line: @line) new_lines = whole_lines(index: @previous_line_index, line: @line)
prompt, prompt_width, = check_multiline_prompt(new_lines, prompt) prompt, prompt_width, = check_multiline_prompt(new_lines, prompt)
@buffer_of_lines[@previous_line_index] = @line @buffer_of_lines[@previous_line_index] = @line
@line = @buffer_of_lines[@line_index] @line = @buffer_of_lines[@line_index]
render_partial(prompt, prompt_width, @line, false) render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
@cursor = @cursor_max = calculate_width(@line) @cursor = @cursor_max = calculate_width(@line)
@byte_pointer = @line.bytesize @byte_pointer = @line.bytesize
@highest_in_all += @highest_in_this @highest_in_all += @highest_in_this
@ -409,14 +455,25 @@ class Reline::LineEditor
else else
calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
end end
@line = @buffer_of_lines[@line_index] first_line_diff = new_first_line_started_from - @first_line_started_from
move_cursor_down(new_first_line_started_from - @first_line_started_from) new_cursor, _, new_started_from, _ = calculate_nearest_cursor(@line, @cursor, @started_from, @byte_pointer, false)
@first_line_started_from = new_first_line_started_from new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
calculate_nearest_cursor calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
move_cursor_down(@started_from)
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
@previous_line_index = nil @previous_line_index = nil
if @rerender_all
@line = @buffer_of_lines[@line_index]
rerender_all_lines
@rerender_all = false
true
else
@line = @buffer_of_lines[@line_index]
@first_line_started_from = new_first_line_started_from
@started_from = new_started_from
@cursor = new_cursor
move_cursor_down(first_line_diff + @started_from)
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
false
end
end end
private def rerender_changed_current_line private def rerender_changed_current_line
@ -479,35 +536,42 @@ class Reline::LineEditor
height = calculate_height_by_width(width) height = calculate_height_by_width(width)
back += height back += height
end end
if back > @highest_in_all old_highest_in_all = @highest_in_all
if @line_index.zero?
new_first_line_started_from = 0
else
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
scroll_down(back - 1) scroll_down(back - 1)
move_cursor_up(back - 1) move_cursor_up(back - 1)
elsif back < @highest_in_all elsif back < old_highest_in_all
scroll_down(back) scroll_down(back)
Reline::IOGate.erase_after_cursor Reline::IOGate.erase_after_cursor
(@highest_in_all - back - 1).times do (old_highest_in_all - back - 1).times do
scroll_down(1) scroll_down(1)
Reline::IOGate.erase_after_cursor Reline::IOGate.erase_after_cursor
end end
move_cursor_up(@highest_in_all - 1) move_cursor_up(old_highest_in_all - 1)
end end
calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
render_whole_lines(new_buffer, prompt_list || prompt, prompt_width) render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
move_cursor_up(back - 1)
if @prompt_proc if @prompt_proc
prompt = prompt_list[@line_index] prompt = prompt_list[@line_index]
prompt_width = calculate_width(prompt, true) prompt_width = calculate_width(prompt, true)
end end
@highest_in_all = back
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
@first_line_started_from = @highest_in_all = back
if @line_index.zero? @first_line_started_from = new_first_line_started_from
0 @started_from = new_started_from
else if @scroll_partial_screen
calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt) Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
end Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 else
move_cursor_down(@first_line_started_from + @started_from) move_cursor_down(@first_line_started_from + @started_from - back + 1)
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
end
end end
private def render_whole_lines(lines, prompt, prompt_width) private def render_whole_lines(lines, prompt, prompt_width)
@ -519,9 +583,15 @@ class Reline::LineEditor
else else
line_prompt = prompt line_prompt = prompt
end end
height = render_partial(line_prompt, prompt_width, line, false) height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
if index < (lines.size - 1) if index < (lines.size - 1)
scroll_down(1) if @scroll_partial_screen
if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
move_cursor_down(1)
end
else
scroll_down(1)
end
rendered_height += height rendered_height += height
else else
rendered_height += height - 1 rendered_height += height - 1
@ -530,8 +600,34 @@ class Reline::LineEditor
rendered_height rendered_height
end end
private def render_partial(prompt, prompt_width, line_to_render, with_control = true) private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last) visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
cursor_up_from_last_line = 0
# TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
if @scroll_partial_screen
last_visual_line = this_started_from + (height - 1)
last_screen_line = @scroll_partial_screen + (@screen_height - 1)
if (@scroll_partial_screen - this_started_from) >= height
# Render nothing because this line is before the screen.
visual_lines = []
elsif this_started_from > last_screen_line
# Render nothing because this line is after the screen.
visual_lines = []
else
deleted_lines_before_screen = []
if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
# A part of visual lines are before the screen.
deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
deleted_lines_before_screen.compact!
end
if this_started_from <= last_screen_line and last_screen_line < last_visual_line
# A part of visual lines are after the screen.
visual_lines.pop((last_visual_line - last_screen_line) * 2)
end
move_cursor_up(deleted_lines_before_screen.size - @started_from)
cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
end
end
if with_control if with_control
if height > @highest_in_this if height > @highest_in_this
diff = height - @highest_in_this diff = height - @highest_in_this
@ -545,10 +641,14 @@ class Reline::LineEditor
@highest_in_this = height @highest_in_this = height
end end
move_cursor_up(@started_from) move_cursor_up(@started_from)
cursor_up_from_last_line = height - 1 - @started_from
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1 @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
end end
Reline::IOGate.move_cursor_column(0) if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
@output.write "\e[0m" # clear character decorations
end
visual_lines.each_with_index do |line, index| visual_lines.each_with_index do |line, index|
Reline::IOGate.move_cursor_column(0)
if line.nil? if line.nil?
if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
# reaches the end of line # reaches the end of line
@ -580,15 +680,18 @@ class Reline::LineEditor
@pre_input_hook&.call @pre_input_hook&.call
end end
end end
Reline::IOGate.erase_after_cursor unless visual_lines.empty?
Reline::IOGate.move_cursor_column(0) Reline::IOGate.erase_after_cursor
Reline::IOGate.move_cursor_column(0)
end
if with_control if with_control
# Just after rendring, so the cursor is on the last line. # Just after rendring, so the cursor is on the last line.
if finished? if finished?
Reline::IOGate.move_cursor_column(0) Reline::IOGate.move_cursor_column(0)
else else
# Moves up from bottom of lines to the cursor position. # Moves up from bottom of lines to the cursor position.
move_cursor_up(height - 1 - @started_from) move_cursor_up(cursor_up_from_last_line)
# This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
end end
end end
@ -624,9 +727,9 @@ class Reline::LineEditor
modify_lines(whole_lines).each_with_index do |line, index| modify_lines(whole_lines).each_with_index do |line, index|
if @prompt_proc if @prompt_proc
pr = prompt_list[index] pr = prompt_list[index]
height = render_partial(pr, calculate_width(pr), line, false) height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
else else
height = render_partial(prompt, prompt_width, line, false) height = render_partial(prompt, prompt_width, line, back, with_control: false)
end end
if index < (@buffer_of_lines.size - 1) if index < (@buffer_of_lines.size - 1)
move_cursor_down(height) move_cursor_down(height)

Просмотреть файл

@ -105,7 +105,8 @@ begin
def test_finish_autowrapped_line_in_the_middle_of_lines def test_finish_autowrapped_line_in_the_middle_of_lines
start_terminal(20, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.') start_terminal(20, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
write("[{'user'=>{'email'=>'abcdef@abcdef', 'id'=>'ABC'}, 'version'=>4, 'status'=>'succeeded'}]#{"\C-b"*7}\n") write("[{'user'=>{'email'=>'abcdef@abcdef', 'id'=>'ABC'}, 'version'=>4, 'status'=>'succeeded'}]#{"\C-b"*7}")
write("\n")
close close
assert_screen(<<~EOC) assert_screen(<<~EOC)
Multiline REPL. Multiline REPL.
@ -452,6 +453,147 @@ begin
EOC EOC
end end
def test_longer_than_screen_height
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
write(<<~EOC.chomp)
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
EOC
close
assert_screen(<<~EOC)
prompt> prompt
prompt> end
prompt> end
prompt> end
prompt> end
EOC
end
def test_longer_than_screen_height_with_scroll_back
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
write(<<~EOC.chomp)
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
EOC
write("\C-p" * 6)
close
assert_screen(<<~EOC)
prompt> rescue Terminate
LineInput
prompt> initialize_inp
ut
prompt> prompt
EOC
end
def test_longer_than_screen_height_with_complex_scroll_back
start_terminal(4, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
write(<<~EOC.chomp)
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
EOC
sleep 0.3
write("\C-p" * 5)
write("\C-n" * 3)
close
assert_screen(<<~EOC)
ut
prompt> prompt
prompt> end
prompt> end
EOC
end
private def write_inputrc(content) private def write_inputrc(content)
File.open(@inputrc_file, 'w') do |f| File.open(@inputrc_file, 'w') do |f|
f.write content f.write content