diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 8153aaba05..bc720c73d2 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -654,13 +654,6 @@ def add_dialog_proc(name, p, context = nil) end end - private def padding_space_with_escape_sequences(str, width) - padding_width = width - calculate_width(str, true) - # padding_width should be only positive value. But macOS and Alacritty returns negative value. - padding_width = 0 if padding_width < 0 - str + (' ' * padding_width) - end - private def render_each_dialog(dialog, cursor_column) if @in_pasting clear_each_dialog(dialog) @@ -753,7 +746,7 @@ def add_dialog_proc(name, p, context = nil) bg_color = dialog_render_info.bg_color end str_width = dialog.width - (dialog.scrollbar_pos.nil? ? 0 : @block_elem_width) - str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width) + str, = Reline::Unicode.take_range(item, 0, str_width, padding: true) @output.write "\e[#{bg_color}m\e[#{fg_color}m#{str}" if dialog.scrollbar_pos and (dialog.scrollbar_pos != old_dialog.scrollbar_pos or dialog.column != old_dialog.column) @output.write "\e[37m" @@ -797,85 +790,42 @@ def add_dialog_proc(name, p, context = nil) end visual_lines.concat(vl) } - old_y = dialog.lines_backup[:first_line_started_from] + dialog.lines_backup[:started_from] - y = @first_line_started_from + @started_from - y_diff = y - old_y - if (old_y + old_dialog.vertical_offset) < (y + dialog.vertical_offset) - # rerender top - move_cursor_down(old_dialog.vertical_offset - y_diff) - start = visual_start + old_dialog.vertical_offset - line_num = dialog.vertical_offset - old_dialog.vertical_offset - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column) - if visual_lines[start + i].nil? - s = ' ' * old_dialog.width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width) - s = padding_space_with_escape_sequences(s, old_dialog.width) - end - @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) - end - move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff) - end - if (old_y + old_dialog.vertical_offset + old_dialog.contents.size) > (y + dialog.vertical_offset + dialog.contents.size) - # rerender bottom - move_cursor_down(dialog.vertical_offset + dialog.contents.size - y_diff) - start = visual_start + dialog.vertical_offset + dialog.contents.size - line_num = (old_dialog.vertical_offset + old_dialog.contents.size) - (dialog.vertical_offset + dialog.contents.size) - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column) - if visual_lines[start + i].nil? - s = ' ' * old_dialog.width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width) - s = padding_space_with_escape_sequences(s, old_dialog.width) + old_dialog_y = dialog.lines_backup[:first_line_started_from] + dialog.lines_backup[:started_from] + dialog_y = @first_line_started_from + @started_from + + x_range = dialog.column...dialog.column + dialog.width + old_x_range = old_dialog.column...old_dialog.column + old_dialog.width + y_range = dialog_y + dialog.vertical_offset...dialog_y + dialog.vertical_offset + dialog.contents.size + old_y_range = old_dialog_y + old_dialog.vertical_offset...old_dialog_y + old_dialog.vertical_offset + old_dialog.contents.size + + cursor_y = dialog_y + old_y_range.each do |y| + rerender_ranges = [] + if y_range.cover?(y) && x_range.any?(old_x_range) + if old_x_range.begin < x_range.begin + # rerender left + rerender_ranges << [old_x_range.begin...[old_x_range.end, x_range.begin].min, true, false] end - @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) - end - move_cursor_up(dialog.vertical_offset + dialog.contents.size + line_num - 1 - y_diff) - end - if old_dialog.column < dialog.column - # rerender left - move_cursor_down(old_dialog.vertical_offset - y_diff) - width = dialog.column - old_dialog.column - start = visual_start + old_dialog.vertical_offset - line_num = old_dialog.contents.size - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column) - if visual_lines[start + i].nil? - s = ' ' * width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, width) - s = padding_space_with_escape_sequences(s, dialog.width) + if x_range.end < old_x_range.end + # rerender right + rerender_ranges << [[x_range.end, old_x_range.begin].max...old_x_range.end, false, true] end + else + rerender_ranges << [old_x_range, true, true] + end + + rerender_ranges.each do |range, cover_begin, cover_end| + move_cursor_down(y - cursor_y) + cursor_y = y + col = range.begin + width = range.end - range.begin + line = visual_lines[y + visual_start - old_dialog_y] || '' + s, col = Reline::Unicode.take_range(line, col, width, cover_begin: cover_begin, cover_end: cover_end, padding: true) + Reline::IOGate.move_cursor_column(col) @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) - end - move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff) - end - if (old_dialog.column + old_dialog.width) > (dialog.column + dialog.width) - # rerender right - move_cursor_down(old_dialog.vertical_offset + y_diff) - width = (old_dialog.column + old_dialog.width) - (dialog.column + dialog.width) - start = visual_start + old_dialog.vertical_offset - line_num = old_dialog.contents.size - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column + dialog.width) - if visual_lines[start + i].nil? - s = ' ' * width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column + dialog.width, width) - rerender_width = old_dialog.width - dialog.width - s = padding_space_with_escape_sequences(s, rerender_width) - end - Reline::IOGate.move_cursor_column(dialog.column + dialog.width) - @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) end - move_cursor_up(old_dialog.vertical_offset + line_num - 1 + y_diff) end + move_cursor_up(cursor_y - dialog_y) Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end @@ -916,9 +866,8 @@ def add_dialog_proc(name, p, context = nil) dialog_vertical_size = dialog.contents.size dialog_vertical_size.times do |i| if i < visual_lines_under_dialog.size - Reline::IOGate.move_cursor_column(dialog.column) - str = Reline::Unicode.take_range(visual_lines_under_dialog[i], dialog.column, dialog.width) - str = padding_space_with_escape_sequences(str, dialog.width) + str, start_pos = Reline::Unicode.take_range(visual_lines_under_dialog[i], dialog.column, dialog.width, cover_begin: true, cover_end: true, padding: true) + Reline::IOGate.move_cursor_column(start_pos) @output.write "\e[0m#{str}\e[0m" else Reline::IOGate.move_cursor_column(dialog.column) diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb index 6000c9f82a..03074dd23f 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -194,17 +194,21 @@ def self.split_by_width(str, max_width, encoding = str.encoding) end # Take a chunk of a String cut by width with escape sequences. - def self.take_range(str, start_col, max_width, encoding = str.encoding) + def self.take_range(str, start_col, width, encoding: str.encoding, cover_begin: false, cover_end: false, padding: false) chunk = String.new(encoding: encoding) total_width = 0 rest = str.encode(Encoding::UTF_8) in_zero_width = false + chunk_start_col = nil + chunk_end_col = nil rest.scan(WIDTH_SCANNER) do |gc| case when gc[NON_PRINTING_START_INDEX] in_zero_width = true + chunk << NON_PRINTING_START when gc[NON_PRINTING_END_INDEX] in_zero_width = false + chunk << NON_PRINTING_END when gc[CSI_REGEXP_INDEX] chunk << gc[CSI_REGEXP_INDEX] when gc[OSC_REGEXP_INDEX] @@ -215,13 +219,31 @@ def self.take_range(str, start_col, max_width, encoding = str.encoding) chunk << gc else mbchar_width = get_mbchar_width(gc) + prev_width = total_width total_width += mbchar_width - break if (start_col + max_width) < total_width - chunk << gc if start_col < total_width + break if !cover_end && total_width > start_col + width + if cover_begin ? start_col < total_width : start_col <= prev_width + chunk << gc + chunk_start_col ||= prev_width + chunk_end_col = total_width + end + break if total_width >= start_col + width end end end - chunk + chunk_start_col ||= start_col + chunk_end_col ||= start_col + if padding + if start_col < chunk_start_col + chunk = ' ' * (chunk_start_col - start_col) + chunk + chunk_start_col = start_col + end + if chunk_end_col < start_col + width + chunk << ' ' * (start_col + width - chunk_end_col) + chunk_end_col = start_col + width + end + end + [chunk, chunk_start_col, chunk_end_col - chunk_start_col] end def self.get_next_mbchar_size(line, byte_pointer) diff --git a/test/reline/test_unicode.rb b/test/reline/test_unicode.rb index 1233e034e8..fc361727fd 100644 --- a/test/reline/test_unicode.rb +++ b/test/reline/test_unicode.rb @@ -19,7 +19,21 @@ def test_ambiguous_width end def test_take_range - assert_equal 'cdef', Reline::Unicode.take_range('abcdefghi', 2, 4) - assert_equal 'いう', Reline::Unicode.take_range('あいうえお', 2, 4) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_range('abcdefghi', 2, 4) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_range('abcdefghi', 2, 4, padding: true) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_range('abcdefghi', 2, 4, cover_begin: true) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_range('abcdefghi', 2, 4, cover_end: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_range('あいうえお', 2, 4) + assert_equal ['いう', 2, 4], Reline::Unicode.take_range('あいうえお', 2, 4, padding: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_range('あいうえお', 2, 4, cover_begin: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_range('あいうえお', 2, 4, cover_end: true) + assert_equal ['う', 4, 2], Reline::Unicode.take_range('あいうえお', 3, 4) + assert_equal [' う ', 3, 4], Reline::Unicode.take_range('あいうえお', 3, 4, padding: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_range('あいうえお', 3, 4, cover_begin: true) + assert_equal ['うえ', 4, 4], Reline::Unicode.take_range('あいうえお', 3, 4, cover_end: true) + assert_equal ['いう ', 2, 5], Reline::Unicode.take_range('あいうえお', 3, 4, cover_begin: true, padding: true) + assert_equal [' うえ', 3, 5], Reline::Unicode.take_range('あいうえお', 3, 4, cover_end: true, padding: true) + assert_equal [' うえお ', 3, 10], Reline::Unicode.take_range('あいうえお', 3, 10, padding: true) + assert_equal ["\e[31mc\1ABC\2d\e[0mef", 2, 4], Reline::Unicode.take_range("\e[31mabc\1ABC\2d\e[0mefghi", 2, 4) end end diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index c383112131..53f6eb67fb 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -1031,6 +1031,22 @@ def test_autocomplete_rerender_under_dialog EOC end + def test_autocomplete_rerender_fullwidth_under_dialog + start_terminal(20, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete}, startup_message: 'Multiline REPL.') + write("def hoge\n\n あいうえおかきくけこab\n aあいうえおかきくけこb\n abあいうえおかきくけこ\C-p\C-p\C-p ") + write('S') + write('t') + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> def hoge + prompt> St + prompt> Stringえおかきくけこab + prompt> Struct えおかきくけこb + prompt> abあいうえおかきくけこ + EOC + end + def test_autocomplete_long_with_scrollbar start_terminal(20, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete-long}, startup_message: 'Multiline REPL.') write('S')