diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 15c3e0b1bf..5f7aa97a7c 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -641,13 +641,6 @@ def add_dialog_proc(name, p, context = nil) render_dialog_changes(changes, cursor_column) 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 range_subtract(base_ranges, subtract_ranges) indices = base_ranges.flat_map(&:to_a).uniq.sort - subtract_ranges.flat_map(&:to_a) chunks = indices.chunk_while { |a, b| a + 1 == b } @@ -723,7 +716,14 @@ def add_dialog_proc(name, p, context = nil) restore_ranges.each do |range| col = range.begin width = range.end - range.begin - s = padding_space_with_escape_sequences(Reline::Unicode.take_range(line, col, width), width) + s, col = Reline::Unicode.take_mbchar_range( + line, + col, + width, + cover_begin: !new_x_ranges&.any? { |new_range| new_range.include? range.begin - 1 }, + cover_end: !new_x_ranges&.any? { |new_range| new_range.include? range.end }, + padding: true + ) Reline::IOGate.move_cursor_column(col) @output.write "\e[0m#{s}\e[0m" end @@ -820,7 +820,7 @@ def add_dialog_proc(name, p, context = nil) bg_color = dialog_render_info.bg_color end str_width = dialog.width - (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_mbchar_range(item, 0, str_width, padding: true) colored_content = "\e[#{bg_color}m\e[#{fg_color}m#{str}" if scrollbar_pos color_seq = "\e[37m" diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb index 0a7f59cf06..4b63e80c04 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -193,16 +193,24 @@ def self.split_by_width(str, max_width, encoding = str.encoding) # Take a chunk of a String cut by width with escape sequences. def self.take_range(str, start_col, max_width) + take_mbchar_range(str, start_col, max_width).first + end + + def self.take_mbchar_range(str, start_col, width, cover_begin: false, cover_end: false, padding: false) chunk = String.new(encoding: str.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 |non_printing_start, non_printing_end, csi, osc, gc| case when non_printing_start in_zero_width = true + chunk << NON_PRINTING_START when non_printing_end in_zero_width = false + chunk << NON_PRINTING_END when csi chunk << csi when osc @@ -212,13 +220,29 @@ def self.take_range(str, start_col, max_width) 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 + if padding && chunk_start_col.nil? && start_col < prev_width + chunk << ' ' * (prev_width - start_col) + chunk_start_col = start_col + end + 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 && chunk_end_col < start_col + width + chunk << ' ' * (start_col + width - chunk_end_col) + chunk_end_col = start_col + width + 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 834f7114c4..6070e7da33 100644 --- a/test/reline/test_unicode.rb +++ b/test/reline/test_unicode.rb @@ -41,8 +41,8 @@ def test_split_by_width def test_take_range assert_equal 'cdef', Reline::Unicode.take_range('abcdefghi', 2, 4) assert_equal 'あde', Reline::Unicode.take_range('abあdef', 2, 4) - assert_equal 'zerocdef', Reline::Unicode.take_range("ab\1zero\2cdef", 2, 4) - assert_equal 'bzerocde', Reline::Unicode.take_range("ab\1zero\2cdef", 1, 4) + assert_equal "\1zero\2cdef", Reline::Unicode.take_range("ab\1zero\2cdef", 2, 4) + assert_equal "b\1zero\2cde", Reline::Unicode.take_range("ab\1zero\2cdef", 1, 4) assert_equal "\e[31mcd\e[42mef", Reline::Unicode.take_range("\e[31mabcd\e[42mefg", 2, 4) assert_equal "\e]0;1\acd", Reline::Unicode.take_range("ab\e]0;1\acd", 2, 3) assert_equal 'いう', Reline::Unicode.take_range('あいうえお', 2, 4) @@ -62,4 +62,24 @@ def test_calculate_width assert_equal 10, Reline::Unicode.calculate_width('あいうえお') assert_equal 10, Reline::Unicode.calculate_width('あいうえお', true) end + + def test_take_mbchar_range + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4, padding: true) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4, cover_begin: true) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4, cover_end: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4, padding: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4, cover_begin: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4, cover_end: true) + assert_equal ['う', 4, 2], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4) + assert_equal [' う ', 3, 4], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, padding: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_begin: true) + assert_equal ['うえ', 4, 4], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_end: true) + assert_equal ['いう ', 2, 5], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_begin: true, padding: true) + assert_equal [' うえ', 3, 5], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_end: true, padding: true) + assert_equal [' うえお ', 3, 10], Reline::Unicode.take_mbchar_range('あいうえお', 3, 10, padding: true) + assert_equal ["\e[31mc\1ABC\2d\e[0mef", 2, 4], Reline::Unicode.take_mbchar_range("\e[31mabc\1ABC\2d\e[0mefghi", 2, 4) + assert_equal ["\e[47m い ", 1, 4], Reline::Unicode.take_mbchar_range("\e[47mあいうえお\e[0m", 1, 4, padding: true) + end end diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 49d8ed406e..0463a14251 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -1155,6 +1155,25 @@ def test_rerender_multiple_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かきくけこab\n aあいうえおabかきくけこb\n abあいうえおabかきくけこ\C-p\C-p\C-p ") + write('S') + write('t') + write(' ') + write('S') + write('t') + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> def hoge + prompt> St St + prompt> あいうえおabStringけこab + prompt> aあいうえおaStruct けこb + prompt> abあいうえお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')