From 853beb2719b12823fb29833821ae18d0c94298ca Mon Sep 17 00:00:00 2001 From: tompng Date: Sun, 13 Oct 2024 17:55:48 +0900 Subject: [PATCH] Fix completion quote, preposing and target calculation bug --- lib/reline/line_editor.rb | 79 ++++++++--------------------- test/reline/test_key_actor_emacs.rb | 22 -------- test/reline/test_line_editor.rb | 60 ++++++++++++++++++++++ 3 files changed, 82 insertions(+), 79 deletions(-) diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index b815659713..a8ba574f9a 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -1241,70 +1241,35 @@ def set_current_lines(lines, byte_pointer = nil, line_index = 0) end def retrieve_completion_block(set_completion_quote_character = false) - if Reline.completer_word_break_characters.empty? - word_break_regexp = nil - else - word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ - end - if Reline.completer_quote_characters.empty? - quote_characters_regexp = nil - else - quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ - end - before = current_line.byteslice(0, @byte_pointer) - rest = nil - break_pointer = nil + quote_characters = Reline.completer_quote_characters + before = current_line.byteslice(0, @byte_pointer).grapheme_clusters quote = nil - closing_quote = nil - escaped_quote = nil - i = 0 - while i < @byte_pointer do - slice = current_line.byteslice(i, @byte_pointer - i) - unless slice.valid_encoding? - i += 1 - next - end - if quote and slice.start_with?(closing_quote) - quote = nil - i += 1 - rest = nil - elsif quote and slice.start_with?(escaped_quote) - # skip - i += 2 - elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new " - rest = $' - quote = $& - closing_quote = /(?!\\)#{Regexp.escape(quote)}/ - escaped_quote = /\\#{Regexp.escape(quote)}/ - i += 1 - break_pointer = i - 1 - elsif word_break_regexp and not quote and slice =~ word_break_regexp - rest = $' - i += 1 - before = current_line.byteslice(i, @byte_pointer - i) - break_pointer = i - else - i += 1 + unless quote_characters.empty? + escaped = false + before.each do |c| + if escaped + escaped = false + next + elsif c == '\\' + escaped = true + elsif quote + quote = nil if c == quote + elsif quote_characters.include?(c) + quote = c + end end end + word_break_characters = quote_characters + Reline.completer_word_break_characters + break_index = before.rindex { |c| word_break_characters.include?(c) || quote_characters.include?(c) } || -1 + preposing = before.take(break_index + 1).join + target = before.drop(break_index + 1).join postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer) - if rest - preposing = current_line.byteslice(0, break_pointer) - target = rest + if target if set_completion_quote_character and quote Reline.core.instance_variable_set(:@completion_quote_character, quote) - if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote - insert_text(quote) - end - end - else - preposing = '' - if break_pointer - preposing = current_line.byteslice(0, break_pointer) - else - preposing = '' + insert_text(quote) # FIXME: should not be here + target += quote end - target = before end lines = whole_lines if @line_index > 0 diff --git a/test/reline/test_key_actor_emacs.rb b/test/reline/test_key_actor_emacs.rb index dc12c803f5..0697667610 100644 --- a/test/reline/test_key_actor_emacs.rb +++ b/test/reline/test_key_actor_emacs.rb @@ -853,28 +853,6 @@ def test_completion_with_indent assert_equal(%w{foo_foo foo_bar foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) end - def test_completion_with_indent_and_completer_quote_characters - @line_editor.completion_proc = proc { |word| - %w{ - "".foo_foo - "".foo_bar - "".foo_baz - "".qux - }.map { |i| - i.encode(@encoding) - } - } - input_keys(' "".fo') - assert_line_around_cursor(' "".fo', '') - assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) - input_keys("\C-i", false) - assert_line_around_cursor(' "".foo_', '') - assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) - input_keys("\C-i", false) - assert_line_around_cursor(' "".foo_', '') - assert_equal(%w{"".foo_foo "".foo_bar "".foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) - end - def test_completion_with_perfect_match @line_editor.completion_proc = proc { |word| %w{ diff --git a/test/reline/test_line_editor.rb b/test/reline/test_line_editor.rb index 1859da8199..e10b2e939d 100644 --- a/test/reline/test_line_editor.rb +++ b/test/reline/test_line_editor.rb @@ -3,6 +3,66 @@ require 'stringio' class Reline::LineEditor + + class CompletionBlockTest < Reline::TestCase + def setup + @original_quote_characters = Reline.completer_quote_characters + @original_word_break_characters = Reline.completer_word_break_characters + @line_editor = Reline::LineEditor.new(nil, Encoding::UTF_8) + end + + def retrieve_completion_block(lines, line_index, byte_pointer) + @line_editor.instance_variable_set(:@buffer_of_lines, lines) + @line_editor.instance_variable_set(:@line_index, line_index) + @line_editor.instance_variable_set(:@byte_pointer, byte_pointer) + @line_editor.retrieve_completion_block(false) + end + + def retrieve_completion_quote(line) + retrieve_completion_block([line], 0, line.bytesize) + _, target = @line_editor.retrieve_completion_block(false) + _, target2 = @line_editor.retrieve_completion_block(true) + # This is a hack to get the quoted character. + # retrieve_completion_block should be refactored to return the quoted character. + target2.chars.last if target2 != target + end + + def teardown + Reline.completer_quote_characters = @original_quote_characters + Reline.completer_word_break_characters = @original_word_break_characters + end + + def test_retrieve_completion_block + Reline.completer_word_break_characters = ' ([{' + Reline.completer_quote_characters = '' + assert_equal(['', '', 'foo'], retrieve_completion_block(['foo'], 0, 0)) + assert_equal(['', 'f', 'oo'], retrieve_completion_block(['foo'], 0, 1)) + assert_equal(['foo ', 'ba', 'r baz'], retrieve_completion_block(['foo bar baz'], 0, 6)) + assert_equal(['foo([', 'b', 'ar])baz'], retrieve_completion_block(['foo([bar])baz'], 0, 6)) + assert_equal(['foo([{', '', '}])baz'], retrieve_completion_block(['foo([{}])baz'], 0, 6)) + assert_equal(["abc\nfoo ", 'ba', "r baz\ndef"], retrieve_completion_block(['abc', 'foo bar baz', 'def'], 1, 6)) + end + + def test_retrieve_completion_block_with_quote_characters + Reline.completer_word_break_characters = ' ([{' + Reline.completer_quote_characters = '' + assert_equal(['"" ', '"wo', 'rd'], retrieve_completion_block(['"" "word'], 0, 6)) + Reline.completer_quote_characters = '"' + assert_equal(['"" "', 'wo', 'rd'], retrieve_completion_block(['"" "word'], 0, 6)) + end + + def test_retrieve_completion_quote + Reline.completer_quote_characters = '"\'' + assert_equal('"', retrieve_completion_quote('"\'')) + assert_equal(nil, retrieve_completion_quote('""')) + assert_equal("'", retrieve_completion_quote('""\'"')) + assert_equal(nil, retrieve_completion_quote('""\'\'')) + assert_equal('"', retrieve_completion_quote('"\\"')) + assert_equal(nil, retrieve_completion_quote('"\\""')) + assert_equal(nil, retrieve_completion_quote('"\\\\"')) + end + end + class RenderLineDifferentialTest < Reline::TestCase class TestIO < Reline::IO def move_cursor_column(col)