From 2b8fa7817643a9ac7632b66ffed7cb5fd3753966 Mon Sep 17 00:00:00 2001 From: aycabta Date: Sun, 13 Dec 2020 11:30:05 +0900 Subject: [ruby/reline] Support longer than screen height https://github.com/ruby/reline/commit/e83a3de9ed --- lib/reline/line_editor.rb | 221 +++++++++++++++++++++++++++++++++------------- 1 file changed, 162 insertions(+), 59 deletions(-) (limited to 'lib/reline') diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 65878fe9f3..b80a184fc6 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -123,6 +123,7 @@ class Reline::LineEditor def reset(prompt = '', encoding:) @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y @screen_size = Reline::IOGate.get_screen_size + @screen_height = @screen_size.first reset_variables(prompt, encoding: encoding) @old_trap = Signal.trap('SIGINT') { @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 old_screen_size = @screen_size @screen_size = Reline::IOGate.get_screen_size + @screen_height = @screen_size.first if old_screen_size.last < @screen_size.last # columns increase @rerender_all = true rerender @@ -202,6 +204,7 @@ class Reline::LineEditor @prompt_cache_time = nil @eof = false @continuous_insertion_buffer = String.new(encoding: @encoding) + @scroll_partial_screen = nil reset_line end @@ -287,28 +290,28 @@ class Reline::LineEditor end end - private def calculate_nearest_cursor - @cursor_max = calculate_width(line) + private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true) + new_cursor_max = calculate_width(line_to_calc) new_cursor = 0 new_byte_pointer = 0 height = 1 max_width = @screen_size.last 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 - 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) - cursor_max = @cursor_max - last_width + end_of_line_cursor = new_cursor_max - last_width else - cursor_max = @cursor_max + end_of_line_cursor = new_cursor_max end else - cursor_max = @cursor_max + end_of_line_cursor = new_cursor_max 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) now = new_cursor + mbchar_width - if now > cursor_max or now > @cursor + if now > end_of_line_cursor or now > cursor break end new_cursor += mbchar_width @@ -317,9 +320,15 @@ class Reline::LineEditor end new_byte_pointer += gc.bytesize end - @started_from = height - 1 - @cursor = new_cursor - @byte_pointer = new_byte_pointer + new_started_from = height - 1 + if update + @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 def rerender_all @@ -349,33 +358,37 @@ class Reline::LineEditor if @add_newline_to_end_of_buffer rerender_added_newline @add_newline_to_end_of_buffer = false - elsif @just_cursor_moving and not @rerender_all - just_move_cursor - @just_cursor_moving = false - return - elsif @previous_line_index or new_highest_in_this != @highest_in_this - rerender_changed_current_line - @previous_line_index = nil - rendered = true - elsif @rerender_all - rerender_all_lines - @rerender_all = false - rendered = true + else + if @just_cursor_moving and not @rerender_all + rendered = just_move_cursor + @just_cursor_moving = false + return + elsif @previous_line_index or new_highest_in_this != @highest_in_this + rerender_changed_current_line + @previous_line_index = nil + rendered = true + elsif @rerender_all + rerender_all_lines + @rerender_all = false + rendered = true + else + end end line = modify_lines(whole_lines)[@line_index] if @is_multiline prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) if finished? # 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) Reline::IOGate.move_cursor_column(0) Reline::IOGate.erase_after_cursor elsif not rendered - render_partial(prompt, prompt_width, line) + render_partial(prompt, prompt_width, line, @first_line_started_from) end + @buffer_of_lines[@line_index] = @line else - render_partial(prompt, prompt_width, line) + render_partial(prompt, prompt_width, line, 0) if finished? scroll_down(1) Reline::IOGate.move_cursor_column(0) @@ -384,13 +397,46 @@ class Reline::LineEditor 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 scroll_down(1) new_lines = whole_lines(index: @previous_line_index, line: @line) prompt, prompt_width, = check_multiline_prompt(new_lines, prompt) @buffer_of_lines[@previous_line_index] = @line @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) @byte_pointer = @line.bytesize @highest_in_all += @highest_in_this @@ -409,14 +455,25 @@ class Reline::LineEditor else calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) end - @line = @buffer_of_lines[@line_index] - move_cursor_down(new_first_line_started_from - @first_line_started_from) - @first_line_started_from = new_first_line_started_from - calculate_nearest_cursor - @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) + first_line_diff = new_first_line_started_from - @first_line_started_from + new_cursor, _, new_started_from, _ = calculate_nearest_cursor(@line, @cursor, @started_from, @byte_pointer, false) + new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1 + calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from) @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 private def rerender_changed_current_line @@ -479,35 +536,42 @@ class Reline::LineEditor height = calculate_height_by_width(width) back += height 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) move_cursor_up(back - 1) - elsif back < @highest_in_all + elsif back < old_highest_in_all scroll_down(back) Reline::IOGate.erase_after_cursor - (@highest_in_all - back - 1).times do + (old_highest_in_all - back - 1).times do scroll_down(1) Reline::IOGate.erase_after_cursor end - move_cursor_up(@highest_in_all - 1) + move_cursor_up(old_highest_in_all - 1) end + calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from) render_whole_lines(new_buffer, prompt_list || prompt, prompt_width) - move_cursor_up(back - 1) if @prompt_proc prompt = prompt_list[@line_index] prompt_width = calculate_width(prompt, true) end - @highest_in_all = back @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) - @first_line_started_from = - if @line_index.zero? - 0 - else - calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt) - end - @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 - move_cursor_down(@first_line_started_from + @started_from) - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) + @highest_in_all = back + @first_line_started_from = new_first_line_started_from + @started_from = new_started_from + if @scroll_partial_screen + Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1) + Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) + else + move_cursor_down(@first_line_started_from + @started_from - back + 1) + Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) + end end private def render_whole_lines(lines, prompt, prompt_width) @@ -519,9 +583,15 @@ class Reline::LineEditor else line_prompt = prompt 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) - 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 else rendered_height += height - 1 @@ -530,8 +600,34 @@ class Reline::LineEditor rendered_height 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) + 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 height > @highest_in_this diff = height - @highest_in_this @@ -545,10 +641,14 @@ class Reline::LineEditor @highest_in_this = height end move_cursor_up(@started_from) + cursor_up_from_last_line = height - 1 - @started_from @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 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| + Reline::IOGate.move_cursor_column(0) if line.nil? if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last # reaches the end of line @@ -580,15 +680,18 @@ class Reline::LineEditor @pre_input_hook&.call end end - Reline::IOGate.erase_after_cursor - Reline::IOGate.move_cursor_column(0) + unless visual_lines.empty? + Reline::IOGate.erase_after_cursor + Reline::IOGate.move_cursor_column(0) + end if with_control # Just after rendring, so the cursor is on the last line. if finished? Reline::IOGate.move_cursor_column(0) else # 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) end end @@ -624,9 +727,9 @@ class Reline::LineEditor modify_lines(whole_lines).each_with_index do |line, index| if @prompt_proc 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 - height = render_partial(prompt, prompt_width, line, false) + height = render_partial(prompt, prompt_width, line, back, with_control: false) end if index < (@buffer_of_lines.size - 1) move_cursor_down(height) -- cgit v1.2.3