diff --git a/lib/reline.rb b/lib/reline.rb index 03e4b745cf..ab9d8c42f2 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -47,6 +47,7 @@ class Core filename_quote_characters special_prefixes completion_proc + completion_filter_proc output_modifier_proc prompt_proc auto_indent_proc @@ -134,6 +135,11 @@ def completion_proc=(p) @completion_proc = p end + def completion_filter_proc=(p) + raise ArgumentError unless p.respond_to?(:call) or p.nil? + @completion_filter_proc = p + end + def output_modifier_proc=(p) raise ArgumentError unless p.respond_to?(:call) or p.nil? @output_modifier_proc = p @@ -318,6 +324,7 @@ def readline(prompt = '', add_hist = false) line_editor.multiline_off end line_editor.completion_proc = completion_proc + line_editor.completion_filter_proc = completion_filter_proc line_editor.completion_append_character = completion_append_character line_editor.output_modifier_proc = output_modifier_proc line_editor.prompt_proc = prompt_proc diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index bfffd17d59..5b99c43f3e 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -8,6 +8,7 @@ class Reline::LineEditor attr_reader :byte_pointer attr_accessor :confirm_multiline_termination_proc attr_accessor :completion_proc + attr_writer :completion_filter_proc attr_accessor :completion_append_character attr_accessor :output_modifier_proc attr_accessor :prompt_proc @@ -799,6 +800,14 @@ def editing_mode @menu_info = MenuInfo.new(list) end + private def completion_filter_proc + @completion_filter_proc || if @config.completion_ignore_case + ->(target, candidate) { candidate.downcase.start_with?(target) } + else + ->(target, candidate) { candidate.start_with?(target) } + end + end + private def filter_normalize_candidates(target, list) target = target.downcase if @config.completion_ignore_case list.select do |item| @@ -810,11 +819,7 @@ def editing_mode end end - if @config.completion_ignore_case - item.downcase.start_with?(target) - else - item.start_with?(target) - end + completion_filter_proc.(target, item) end.map do |item| item.unicode_normalize rescue Encoding::CompatibilityError @@ -894,7 +899,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.select(&completion_filter_proc.curry.(target)) return if candidates.empty? pre = preposing.split("\n", -1).last || '' diff --git a/test/reline/test_key_actor_emacs.rb b/test/reline/test_key_actor_emacs.rb index aa608641c3..307ce6c312 100644 --- a/test/reline/test_key_actor_emacs.rb +++ b/test/reline/test_key_actor_emacs.rb @@ -1018,6 +1018,30 @@ def test_completion_with_completion_ignore_case assert_line_around_cursor('Foo_baz', '') end + def test_completion_with_completion_filter_proc + @line_editor.completion_proc = proc { |word| + %w{ + foo_bar + foo_baz + qux + }.map { |i| + i.encode(@encoding) + } + } + # Fuzzy matching: characters appear in order but not necessarily contiguous + @line_editor.completion_filter_proc = ->(target, candidate) { + idx = 0 + target.each_char.all? { |c| (idx = candidate.index(c, idx)) && (idx += 1) } + } + input_keys('fb') + assert_line_around_cursor('fb', '') + input_keys("\C-i") + # 'fb' fuzzy matches 'foo_bar' and 'foo_baz' (f...b) + assert_line_around_cursor('foo_ba', '') + input_keys("\C-i") + assert_equal(%w{foo_bar foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) + end + def test_completion_in_middle_of_line @line_editor.completion_proc = proc { |word| %w{ diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index aa0fd7d29a..fb6cd3c959 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -15,6 +15,7 @@ def setup Reline.send(:test_mode) Reline.output_modifier_proc = nil Reline.completion_proc = nil + Reline.completion_filter_proc = nil Reline.prompt_proc = nil Reline.auto_indent_proc = nil Reline.pre_input_hook = nil @@ -163,6 +164,28 @@ def test_completion_proc assert_equal(dummy, Reline.completion_proc) end + def test_completion_filter_proc + assert_equal(nil, Reline.completion_filter_proc) + + dummy_proc = proc {} + Reline.completion_filter_proc = dummy_proc + assert_equal(dummy_proc, Reline.completion_filter_proc) + + l = lambda {} + Reline.completion_filter_proc = l + assert_equal(l, Reline.completion_filter_proc) + + assert_raise(ArgumentError) { Reline.completion_filter_proc = 42 } + assert_raise(ArgumentError) { Reline.completion_filter_proc = "hoge" } + + dummy = DummyCallbackObject.new + Reline.completion_filter_proc = dummy + assert_equal(dummy, Reline.completion_filter_proc) + + Reline.completion_filter_proc = nil + assert_nil(Reline.completion_filter_proc) + end + def test_output_modifier_proc assert_equal(nil, Reline.output_modifier_proc)