diff options
author | tomoya ishida <[email protected]> | 2024-11-12 00:31:01 +0900 |
---|---|---|
committer | git <[email protected]> | 2024-11-11 15:31:04 +0000 |
commit | 25d17868de058cd857ec04496222b101be9a5429 (patch) | |
tree | c486139b4dbe9b4d08e59b8a59dc294fcff8e188 | |
parent | 3ac5c053279c9a567fe1fa336840fd86f730c5e5 (diff) |
[ruby/reline] Refactor perform_completon
(https://github.com/ruby/reline/pull/778)
Flatten recursive method
Remove CompletionState::COMPLETE
https://github.com/ruby/reline/commit/aa5b278f3d
-rw-r--r-- | lib/reline/line_editor.rb | 138 | ||||
-rw-r--r-- | lib/reline/unicode.rb | 13 | ||||
-rw-r--r-- | test/reline/test_key_actor_emacs.rb | 3 | ||||
-rw-r--r-- | test/reline/test_unicode.rb | 10 |
4 files changed, 80 insertions, 84 deletions
diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index c0c8f499d3..c8a7fc9fea 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -36,7 +36,6 @@ class Reline::LineEditor module CompletionState NORMAL = :normal - COMPLETION = :completion MENU = :menu MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match PERFECT_MATCH = :perfect_match @@ -800,105 +799,74 @@ class Reline::LineEditor @config.editing_mode end - private def menu(_target, list) + private def menu(list) @menu_info = MenuInfo.new(list) end - private def complete_internal_proc(list, is_menu) - preposing, target, postposing = retrieve_completion_block - candidates = list.select { |i| - if i and not Encoding.compatible?(target.encoding, i.encoding) - raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{i.encoding.name}" + 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) + # Crash with Encoding::CompatibilityError is required by readline-ext/test/readline/test_readline.rb + # TODO: fix the test + raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{item.encoding.name}" end + if @config.completion_ignore_case - i&.downcase&.start_with?(target.downcase) + item.downcase.start_with?(target) else - i&.start_with?(target) - end - }.uniq - if is_menu - menu(target, candidates) - return nil - end - completed = candidates.inject { |memo, item| - begin - memo_mbchars = memo.unicode_normalize.grapheme_clusters - item_mbchars = item.unicode_normalize.grapheme_clusters - rescue Encoding::CompatibilityError - memo_mbchars = memo.grapheme_clusters - item_mbchars = item.grapheme_clusters - end - size = [memo_mbchars.size, item_mbchars.size].min - result = +'' - size.times do |i| - if @config.completion_ignore_case - if memo_mbchars[i].casecmp?(item_mbchars[i]) - result << memo_mbchars[i] - else - break - end - else - if memo_mbchars[i] == item_mbchars[i] - result << memo_mbchars[i] - else - break - end - end + item.start_with?(target) end - result - } - - [target, preposing, completed, postposing, candidates] + end.map do |item| + item.unicode_normalize + rescue Encoding::CompatibilityError + item + end.uniq end - private def perform_completion(list, just_show_list) + private def perform_completion(list) + preposing, target, postposing = retrieve_completion_block + candidates = filter_normalize_candidates(target, list) + case @completion_state - when CompletionState::NORMAL - @completion_state = CompletionState::COMPLETION when CompletionState::PERFECT_MATCH if @dig_perfect_match_proc - @dig_perfect_match_proc.(@perfect_matched) - else - @completion_state = CompletionState::COMPLETION + @dig_perfect_match_proc.call(@perfect_matched) + return end - end - if just_show_list - is_menu = true - elsif @completion_state == CompletionState::MENU - is_menu = true - elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH - is_menu = true - else - is_menu = false - end - result = complete_internal_proc(list, is_menu) - if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH + when CompletionState::MENU + menu(candidates) + return + when CompletionState::MENU_WITH_PERFECT_MATCH + menu(candidates) @completion_state = CompletionState::PERFECT_MATCH + return end - return if result.nil? - target, preposing, completed, postposing, candidates = result - return if completed.nil? - if target <= completed and (@completion_state == CompletionState::COMPLETION) - append_character = '' - if candidates.include?(completed) - if candidates.one? - append_character = completion_append_character.to_s - @completion_state = CompletionState::PERFECT_MATCH - else - @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH - perform_completion(candidates, true) if @config.show_all_if_ambiguous - end - @perfect_matched = completed + + completed = Reline::Unicode.common_prefix(candidates, ignore_case: @config.completion_ignore_case) + return if completed.empty? + + append_character = '' + if candidates.include?(completed) + if candidates.one? + append_character = completion_append_character.to_s + @completion_state = CompletionState::PERFECT_MATCH + elsif @config.show_all_if_ambiguous + menu(candidates) + @completion_state = CompletionState::PERFECT_MATCH else - @completion_state = CompletionState::MENU - perform_completion(candidates, true) if @config.show_all_if_ambiguous - end - unless just_show_list - @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 + @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH end + @perfect_matched = completed + else + @completion_state = CompletionState::MENU + menu(candidates) if @config.show_all_if_ambiguous end + @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 end def dialog_proc_scope_completion_journey_data @@ -1463,7 +1431,7 @@ class Reline::LineEditor result = call_completion_proc if result.is_a?(Array) @completion_occurs = true - perform_completion(result, false) + perform_completion(result) end end end @@ -1929,7 +1897,9 @@ class Reline::LineEditor elsif [email protected] # show completed list result = call_completion_proc if result.is_a?(Array) - perform_completion(result, true) + _preposing, target = retrieve_completion_block + candidates = filter_normalize_candidates(target, result) + menu(candidates) end end end diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb index 7bca22aeed..f8eb365069 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -633,6 +633,19 @@ class Reline::Unicode byte_size end + def self.common_prefix(list, ignore_case: false) + return '' if list.empty? + + common_prefix_gcs = list.first.grapheme_clusters + list.each do |item| + gcs = item.grapheme_clusters + common_prefix_gcs = common_prefix_gcs.take_while.with_index do |gc, i| + ignore_case ? gc.casecmp?(gcs[i]) : gc == gcs[i] + end + end + common_prefix_gcs.join + end + def self.vi_first_print(line) byte_size = 0 while (line.bytesize - 1) > byte_size diff --git a/test/reline/test_key_actor_emacs.rb b/test/reline/test_key_actor_emacs.rb index a7e60a509b..d07a2ee3c3 100644 --- a/test/reline/test_key_actor_emacs.rb +++ b/test/reline/test_key_actor_emacs.rb @@ -986,6 +986,9 @@ class Reline::KeyActor::EmacsTest < Reline::TestCase input_keys('b') input_keys("\C-i", false) assert_line_around_cursor('foo_ba', '') + input_keys('Z') + input_keys("\C-i", false) + assert_line_around_cursor('Foo_baz', '') end def test_completion_in_middle_of_line diff --git a/test/reline/test_unicode.rb b/test/reline/test_unicode.rb index a0ed7eed7b..07ed8c6230 100644 --- a/test/reline/test_unicode.rb +++ b/test/reline/test_unicode.rb @@ -107,6 +107,16 @@ class Reline::Unicode::Test < Reline::TestCase assert_equal ["\e[41m \e[42mい\e[43m ", 1, 4], Reline::Unicode.take_mbchar_range("\e[41mあ\e[42mい\e[43mう", 1, 4, padding: true) end + def test_common_prefix + assert_equal('', Reline::Unicode.common_prefix([])) + assert_equal('abc', Reline::Unicode.common_prefix(['abc'])) + assert_equal('12', Reline::Unicode.common_prefix(['123', '123️⃣'])) + assert_equal('', Reline::Unicode.common_prefix(['abc', 'xyz'])) + assert_equal('ab', Reline::Unicode.common_prefix(['abcd', 'abc', 'abx', 'abcd'])) + assert_equal('A', Reline::Unicode.common_prefix(['AbcD', 'ABC', 'AbX', 'AbCD'])) + assert_equal('Ab', Reline::Unicode.common_prefix(['AbcD', 'ABC', 'AbX', 'AbCD'], ignore_case: true)) + end + def test_encoding_conversion texts = [ String.new("invalid\xFFutf8", encoding: 'utf-8'), |