diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index c8b251f78e..577536480e 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -813,23 +813,20 @@ 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 @@ -837,7 +834,8 @@ def editing_mode 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 @@ -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) @@ -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 @@ -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 || '' @@ -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 diff --git a/test/reline/helper.rb b/test/reline/helper.rb index 6f470a617f..ad69760f7a 100644 --- a/test/reline/helper.rb +++ b/test/reline/helper.rb @@ -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) diff --git a/test/reline/test_key_actor_emacs.rb b/test/reline/test_key_actor_emacs.rb index 7c1dfd0189..52dd67430c 100644 --- a/test/reline/test_key_actor_emacs.rb +++ b/test/reline/test_key_actor_emacs.rb @@ -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)) @@ -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)) @@ -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)) @@ -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") @@ -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)) @@ -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 @@ -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") @@ -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', '') @@ -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', '') @@ -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") @@ -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) diff --git a/test/reline/test_key_actor_vi.rb b/test/reline/test_key_actor_vi.rb index 083433f9a8..253545adad 100644 --- a/test/reline/test_key_actor_vi.rb +++ b/test/reline/test_key_actor_vi.rb @@ -562,14 +562,7 @@ def test_vi_list_or_eof_with_non_empty_line end def test_completion_journey - @line_editor.completion_proc = proc { |word| - %w{ - foo_bar - foo_bar_baz - }.map { |i| - i.encode(@encoding) - } - } + set_completion_candidates(%w[foo_bar foo_bar_baz]) input_keys('foo') assert_line_around_cursor('foo', '') input_keys("\C-n") @@ -587,14 +580,7 @@ def test_completion_journey end def test_completion_journey_reverse - @line_editor.completion_proc = proc { |word| - %w{ - foo_bar - foo_bar_baz - }.map { |i| - i.encode(@encoding) - } - } + set_completion_candidates(%w[foo_bar foo_bar_baz]) input_keys('foo') assert_line_around_cursor('foo', '') input_keys("\C-p") @@ -612,14 +598,7 @@ def test_completion_journey_reverse end def test_completion_journey_in_middle_of_line - @line_editor.completion_proc = proc { |word| - %w{ - foo_bar - foo_bar_baz - }.map { |i| - i.encode(@encoding) - } - } + set_completion_candidates(%w[foo_bar foo_bar_baz]) input_keys('abcde fo ABCDE') assert_line_around_cursor('abcde fo ABCDE', '') input_keys("\C-[" + 'h' * 5 + "i\C-n") @@ -639,14 +618,7 @@ def test_completion_journey_in_middle_of_line end def test_completion - @line_editor.completion_proc = proc { |word| - %w{ - foo_bar - foo_bar_baz - }.map { |i| - i.encode(@encoding) - } - } + set_completion_candidates(%w[foo_bar foo_bar_baz]) input_keys('foo') assert_line_around_cursor('foo', '') input_keys("\C-i") @@ -655,15 +627,7 @@ def test_completion def test_autocompletion_with_upward_navigation @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") @@ -678,15 +642,7 @@ def test_autocompletion_with_upward_navigation def test_autocompletion_with_upward_navigation_and_menu_complete_backward @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") @@ -701,14 +657,7 @@ def test_autocompletion_with_upward_navigation_and_menu_complete_backward def test_completion_with_disable_completion @config.disable_completion = true - @line_editor.completion_proc = proc { |word| - %w{ - foo_bar - foo_bar_baz - }.map { |i| - i.encode(@encoding) - } - } + set_completion_candidates(%w[foo_bar foo_bar_baz]) input_keys('foo') assert_line_around_cursor('foo', '') input_keys("\C-i")