Skip to content

Commit d3ba721

Browse files
authored
Fix completion quote, preposing and target calculation bug (#763)
1 parent 3ceba3b commit d3ba721

File tree

3 files changed

+82
-79
lines changed

3 files changed

+82
-79
lines changed

lib/reline/line_editor.rb

Lines changed: 22 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,70 +1225,35 @@ def set_current_lines(lines, byte_pointer = nil, line_index = 0)
12251225
end
12261226

12271227
def retrieve_completion_block(set_completion_quote_character = false)
1228-
if Reline.completer_word_break_characters.empty?
1229-
word_break_regexp = nil
1230-
else
1231-
word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1232-
end
1233-
if Reline.completer_quote_characters.empty?
1234-
quote_characters_regexp = nil
1235-
else
1236-
quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1237-
end
1238-
before = current_line.byteslice(0, @byte_pointer)
1239-
rest = nil
1240-
break_pointer = nil
1228+
quote_characters = Reline.completer_quote_characters
1229+
before = current_line.byteslice(0, @byte_pointer).grapheme_clusters
12411230
quote = nil
1242-
closing_quote = nil
1243-
escaped_quote = nil
1244-
i = 0
1245-
while i < @byte_pointer do
1246-
slice = current_line.byteslice(i, @byte_pointer - i)
1247-
unless slice.valid_encoding?
1248-
i += 1
1249-
next
1250-
end
1251-
if quote and slice.start_with?(closing_quote)
1252-
quote = nil
1253-
i += 1
1254-
rest = nil
1255-
elsif quote and slice.start_with?(escaped_quote)
1256-
# skip
1257-
i += 2
1258-
elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new "
1259-
rest = $'
1260-
quote = $&
1261-
closing_quote = /(?!\\)#{Regexp.escape(quote)}/
1262-
escaped_quote = /\\#{Regexp.escape(quote)}/
1263-
i += 1
1264-
break_pointer = i - 1
1265-
elsif word_break_regexp and not quote and slice =~ word_break_regexp
1266-
rest = $'
1267-
i += 1
1268-
before = current_line.byteslice(i, @byte_pointer - i)
1269-
break_pointer = i
1270-
else
1271-
i += 1
1231+
unless quote_characters.empty?
1232+
escaped = false
1233+
before.each do |c|
1234+
if escaped
1235+
escaped = false
1236+
next
1237+
elsif c == '\\'
1238+
escaped = true
1239+
elsif quote
1240+
quote = nil if c == quote
1241+
elsif quote_characters.include?(c)
1242+
quote = c
1243+
end
12721244
end
12731245
end
1246+
word_break_characters = quote_characters + Reline.completer_word_break_characters
1247+
break_index = before.rindex { |c| word_break_characters.include?(c) || quote_characters.include?(c) } || -1
1248+
preposing = before.take(break_index + 1).join
1249+
target = before.drop(break_index + 1).join
12741250
postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer)
1275-
if rest
1276-
preposing = current_line.byteslice(0, break_pointer)
1277-
target = rest
1251+
if target
12781252
if set_completion_quote_character and quote
12791253
Reline.core.instance_variable_set(:@completion_quote_character, quote)
1280-
if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote
1281-
insert_text(quote)
1282-
end
1283-
end
1284-
else
1285-
preposing = ''
1286-
if break_pointer
1287-
preposing = current_line.byteslice(0, break_pointer)
1288-
else
1289-
preposing = ''
1254+
insert_text(quote) # FIXME: should not be here
1255+
target += quote
12901256
end
1291-
target = before
12921257
end
12931258
lines = whole_lines
12941259
if @line_index > 0

test/reline/test_key_actor_emacs.rb

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -853,28 +853,6 @@ def test_completion_with_indent
853853
assert_equal(%w{foo_foo foo_bar foo_baz}, @line_editor.instance_variable_get(:@menu_info).list)
854854
end
855855

856-
def test_completion_with_indent_and_completer_quote_characters
857-
@line_editor.completion_proc = proc { |word|
858-
%w{
859-
"".foo_foo
860-
"".foo_bar
861-
"".foo_baz
862-
"".qux
863-
}.map { |i|
864-
i.encode(@encoding)
865-
}
866-
}
867-
input_keys(' "".fo')
868-
assert_line_around_cursor(' "".fo', '')
869-
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
870-
input_keys("\C-i", false)
871-
assert_line_around_cursor(' "".foo_', '')
872-
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
873-
input_keys("\C-i", false)
874-
assert_line_around_cursor(' "".foo_', '')
875-
assert_equal(%w{"".foo_foo "".foo_bar "".foo_baz}, @line_editor.instance_variable_get(:@menu_info).list)
876-
end
877-
878856
def test_completion_with_perfect_match
879857
@line_editor.completion_proc = proc { |word|
880858
%w{

test/reline/test_line_editor.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,66 @@
33
require 'stringio'
44

55
class Reline::LineEditor
6+
7+
class CompletionBlockTest < Reline::TestCase
8+
def setup
9+
@original_quote_characters = Reline.completer_quote_characters
10+
@original_word_break_characters = Reline.completer_word_break_characters
11+
@line_editor = Reline::LineEditor.new(nil, Encoding::UTF_8)
12+
end
13+
14+
def retrieve_completion_block(lines, line_index, byte_pointer)
15+
@line_editor.instance_variable_set(:@buffer_of_lines, lines)
16+
@line_editor.instance_variable_set(:@line_index, line_index)
17+
@line_editor.instance_variable_set(:@byte_pointer, byte_pointer)
18+
@line_editor.retrieve_completion_block(false)
19+
end
20+
21+
def retrieve_completion_quote(line)
22+
retrieve_completion_block([line], 0, line.bytesize)
23+
_, target = @line_editor.retrieve_completion_block(false)
24+
_, target2 = @line_editor.retrieve_completion_block(true)
25+
# This is a hack to get the quoted character.
26+
# retrieve_completion_block should be refactored to return the quoted character.
27+
target2.chars.last if target2 != target
28+
end
29+
30+
def teardown
31+
Reline.completer_quote_characters = @original_quote_characters
32+
Reline.completer_word_break_characters = @original_word_break_characters
33+
end
34+
35+
def test_retrieve_completion_block
36+
Reline.completer_word_break_characters = ' ([{'
37+
Reline.completer_quote_characters = ''
38+
assert_equal(['', '', 'foo'], retrieve_completion_block(['foo'], 0, 0))
39+
assert_equal(['', 'f', 'oo'], retrieve_completion_block(['foo'], 0, 1))
40+
assert_equal(['foo ', 'ba', 'r baz'], retrieve_completion_block(['foo bar baz'], 0, 6))
41+
assert_equal(['foo([', 'b', 'ar])baz'], retrieve_completion_block(['foo([bar])baz'], 0, 6))
42+
assert_equal(['foo([{', '', '}])baz'], retrieve_completion_block(['foo([{}])baz'], 0, 6))
43+
assert_equal(["abc\nfoo ", 'ba', "r baz\ndef"], retrieve_completion_block(['abc', 'foo bar baz', 'def'], 1, 6))
44+
end
45+
46+
def test_retrieve_completion_block_with_quote_characters
47+
Reline.completer_word_break_characters = ' ([{'
48+
Reline.completer_quote_characters = ''
49+
assert_equal(['"" ', '"wo', 'rd'], retrieve_completion_block(['"" "word'], 0, 6))
50+
Reline.completer_quote_characters = '"'
51+
assert_equal(['"" "', 'wo', 'rd'], retrieve_completion_block(['"" "word'], 0, 6))
52+
end
53+
54+
def test_retrieve_completion_quote
55+
Reline.completer_quote_characters = '"\''
56+
assert_equal('"', retrieve_completion_quote('"\''))
57+
assert_equal(nil, retrieve_completion_quote('""'))
58+
assert_equal("'", retrieve_completion_quote('""\'"'))
59+
assert_equal(nil, retrieve_completion_quote('""\'\''))
60+
assert_equal('"', retrieve_completion_quote('"\\"'))
61+
assert_equal(nil, retrieve_completion_quote('"\\""'))
62+
assert_equal(nil, retrieve_completion_quote('"\\\\"'))
63+
end
64+
end
65+
666
class RenderLineDifferentialTest < Reline::TestCase
767
class TestIO < Reline::IO
868
def move_cursor_column(col)

0 commit comments

Comments
 (0)