Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -813,31 +813,29 @@ def editing_mode
@menu_info = MenuInfo.new(list)
end

private def filter_normalize_candidates(target, list)
target = target.downcase if @config.completion_ignore_case
list.select do |item|
next unless item
unless Encoding.compatible?(target.encoding, item.encoding)
# Workaround for Readline test
if defined?(::Readline) && ::Readline == ::Reline
# Candidates are not filtered by target. Readline (and readline-ext) treats
# filtering as completion_proc's responsibility and uses the returned
# candidates as-is.
private def normalize_candidates(target, list)
candidates = list.compact
if defined?(::Readline) && ::Readline == ::Reline
# Workaround for Readline test
candidates.each do |item|
unless Encoding.compatible?(target.encoding, item.encoding)
raise Encoding::CompatibilityError, "incompatible character encodings: #{target.encoding} and #{item.encoding}"
end
end

if @config.completion_ignore_case
item.downcase.start_with?(target)
else
item.start_with?(target)
end
end.map do |item|
end
candidates.map do |item|
item.unicode_normalize
rescue Encoding::CompatibilityError
item
end.uniq
end

private def perform_completion(preposing, target, postposing, quote, list)
candidates = filter_normalize_candidates(target, list)
candidates = normalize_candidates(target, list)
return if candidates.empty?

case @completion_state
when CompletionState::PERFECT_MATCH
Expand All @@ -855,7 +853,6 @@ def editing_mode
end

completed = Reline::Unicode.common_prefix(candidates, ignore_case: @config.completion_ignore_case)
return if completed.empty?

append_character = ''
if candidates.include?(completed)
Expand All @@ -873,6 +870,10 @@ def editing_mode
@completion_state = CompletionState::MENU
menu(candidates) if @config.show_all_if_ambiguous
end
# Aligned with GNU readline: TAB completion with no common prefix leaves
# the line as-is but still displays candidates on the next TAB.
return if completed.empty?

@buffer_of_lines[@line_index] = (preposing + completed + append_character + postposing).split("\n")[@line_index] || String.new(encoding: encoding)
line_to_pointer = (preposing + completed + append_character).split("\n")[@line_index] || String.new(encoding: encoding)
@byte_pointer = line_to_pointer.bytesize
Expand Down Expand Up @@ -908,7 +909,7 @@ def dialog_proc_scope_completion_journey_data
list = call_completion_proc(preposing, target, postposing, quote)
return unless list.is_a?(Array)

candidates = list.select{ |item| item.start_with?(target) }
candidates = list.compact
return if candidates.empty?

pre = preposing.split("\n", -1).last || ''
Expand Down Expand Up @@ -1784,7 +1785,7 @@ def finish
pre, target, post, quote = retrieve_completion_block
result = call_completion_proc(pre, target, post, quote)
if result.is_a?(Array)
candidates = filter_normalize_candidates(target, result)
candidates = normalize_candidates(target, result)
menu(candidates)
end
end
Expand Down
11 changes: 11 additions & 0 deletions test/reline/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ def input_keys(input)
end
end

# Readline-compatible completion_proc: prefix-filtering candidates is
# completion_proc's responsibility, not Reline's.
def set_completion_candidates(candidates, ignore_case: false)
@line_editor.completion_proc = proc { |word|
word = word.downcase if ignore_case
candidates.select { |s|
(ignore_case ? s.downcase : s).start_with?(word)
}.map { |s| convert_str(s) }
}
end

def set_line_around_cursor(before, after)
input_keys("\C-a\C-k")
input_keys(after)
Expand Down
125 changes: 39 additions & 86 deletions test/reline/test_key_actor_emacs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -740,16 +740,7 @@ def test_em_upper_case_with_complex_example
end

def test_em_delete_or_list
@line_editor.completion_proc = proc { |word|
%w{
foo_foo
foo_bar
foo_baz
qux
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[foo_foo foo_bar foo_baz qux])
input_keys('fooo')
assert_line_around_cursor('fooo', '')
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
Expand All @@ -765,15 +756,7 @@ def test_em_delete_or_list
end

def test_completion_duplicated_list
@line_editor.completion_proc = proc { |word|
%w{
foo_foo
foo_foo
foo_bar
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[foo_foo foo_foo foo_bar])
input_keys('foo_')
assert_line_around_cursor('foo_', '')
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
Expand All @@ -786,16 +769,7 @@ def test_completion_duplicated_list
end

def test_completion
@line_editor.completion_proc = proc { |word|
%w{
foo_foo
foo_bar
foo_baz
qux
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[foo_foo foo_bar foo_baz qux])
input_keys('fo')
assert_line_around_cursor('fo', '')
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
Expand Down Expand Up @@ -829,15 +803,7 @@ def test_completion

def test_autocompletion
@config.autocompletion = true
@line_editor.completion_proc = proc { |word|
%w{
Readline
Regexp
RegexpError
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[Readline Regexp RegexpError])
input_keys('Re')
assert_line_around_cursor('Re', '')
input_keys("\C-i")
Expand All @@ -857,16 +823,7 @@ def test_autocompletion
end

def test_completion_with_indent
@line_editor.completion_proc = proc { |word|
%w{
foo_foo
foo_bar
foo_baz
qux
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[foo_foo foo_bar foo_baz qux])
input_keys(' fo')
assert_line_around_cursor(' fo', '')
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
Expand All @@ -879,14 +836,7 @@ def test_completion_with_indent
end

def test_completion_with_perfect_match
@line_editor.completion_proc = proc { |word|
%w{
foo
foo_bar
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[foo foo_bar])
matched = nil
@line_editor.dig_perfect_match_proc = proc { |m|
matched = m
Expand Down Expand Up @@ -943,9 +893,7 @@ def test_continuous_completion_disabled_with_perfect_match
end

def test_completion_append_character
@line_editor.completion_proc = proc { |word|
%w[foo_ foo_foo foo_bar].select { |s| s.start_with? word }
}
set_completion_candidates(%w[foo_ foo_foo foo_bar])
@line_editor.completion_append_character = 'X'
input_keys('f')
input_keys("\C-i")
Expand All @@ -959,9 +907,7 @@ def test_completion_append_character
end

def test_completion_with_quote_append
@line_editor.completion_proc = proc { |word|
%w[foo bar baz].select { |s| s.start_with? word }
}
set_completion_candidates(%w[foo bar baz])
set_line_around_cursor('x = "b', '')
input_keys("\C-i")
assert_line_around_cursor('x = "ba', '')
Expand All @@ -983,29 +929,22 @@ def test_completion_with_quote_append
end

def test_completion_with_completion_ignore_case
@line_editor.completion_proc = proc { |word|
%w{
foo_foo
foo_bar
Foo_baz
qux
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[foo_foo foo_bar Foo_baz qux], ignore_case: true)
input_keys('fo')
assert_line_around_cursor('fo', '')
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
input_keys("\C-i")
assert_line_around_cursor('foo_', '')
# completion_ignore_case is false: the candidates have no common prefix
assert_line_around_cursor('fo', '')
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
input_keys("\C-i")
assert_line_around_cursor('foo_', '')
assert_equal(%w{foo_foo foo_bar}, @line_editor.instance_variable_get(:@menu_info).list)
assert_line_around_cursor('fo', '')
assert_equal(%w{foo_foo foo_bar Foo_baz}, @line_editor.instance_variable_get(:@menu_info).list)
@config.completion_ignore_case = true
input_keys('o')
input_keys("\C-i")
# completion_ignore_case affects the common prefix computation
assert_line_around_cursor('foo_', '')
assert_equal(%w{foo_foo foo_bar Foo_baz}, @line_editor.instance_variable_get(:@menu_info).list)
input_keys('a')
input_keys("\C-i")
assert_line_around_cursor('foo_a', '')
Expand All @@ -1018,17 +957,29 @@ def test_completion_with_completion_ignore_case
assert_line_around_cursor('Foo_baz', '')
end

# Readline never filters candidates by the input word. Filtering is
# completion_proc's responsibility.
def test_completion_does_not_filter_candidates
@line_editor.completion_proc = proc { |word| %w{ABCX abcy} }
input_keys('z')
input_keys("\C-i")
assert_line_around_cursor('z', '')
assert_equal(nil, @line_editor.instance_variable_get(:@menu_info))
input_keys("\C-i")
assert_line_around_cursor('z', '')
assert_equal(%w{ABCX abcy}, @line_editor.instance_variable_get(:@menu_info).list)
end

def test_completion_replaces_word_with_ignore_case_common_prefix
@config.completion_ignore_case = true
@line_editor.completion_proc = proc { |word| %w{ABCX abcy} }
input_keys('z')
input_keys("\C-i")
assert_line_around_cursor('ABC', '')
end

def test_completion_in_middle_of_line
@line_editor.completion_proc = proc { |word|
%w{
foo_foo
foo_bar
foo_baz
qux
}.map { |i|
i.encode(@encoding)
}
}
set_completion_candidates(%w[foo_foo foo_bar foo_baz qux])
input_keys('abcde fo ABCDE')
assert_line_around_cursor('abcde fo ABCDE', '')
input_keys("\C-b" * 6 + "\C-i")
Expand All @@ -1044,6 +995,8 @@ def test_completion_with_nil_value
foo_bar
Foo_baz
qux
}.select { |s|
s.downcase.start_with?(word.downcase)
}.map { |i|
i.encode(@encoding)
}.prepend(nil)
Expand Down
Loading