From 265959e5477609b092c43dfa294e8b24f0ea8398 Mon Sep 17 00:00:00 2001 From: tompng Date: Mon, 27 May 2024 02:29:02 +0900 Subject: [PATCH 1/4] Improve key binding match/matching check --- lib/reline.rb | 33 ++----- lib/reline/config.rb | 36 ++++---- lib/reline/key_actor.rb | 1 + lib/reline/key_actor/base.rb | 28 ++++-- lib/reline/key_actor/composite.rb | 17 ++++ lib/reline/key_actor/emacs.rb | 4 +- lib/reline/key_actor/vi_command.rb | 4 +- lib/reline/key_actor/vi_insert.rb | 4 +- lib/reline/key_stroke.rb | 138 +++++++--------------------- lib/reline/line_editor.rb | 4 +- test/reline/helper.rb | 2 +- test/reline/test_config.rb | 72 +++++++++------ test/reline/test_key_actor_emacs.rb | 2 +- test/reline/test_key_actor_vi.rb | 42 +++++---- test/reline/test_key_stroke.rb | 32 +++---- test/reline/test_reline.rb | 4 +- test/reline/test_reline_key.rb | 51 +--------- 17 files changed, 198 insertions(+), 276 deletions(-) create mode 100644 lib/reline/key_actor/composite.rb diff --git a/lib/reline.rb b/lib/reline.rb index 796e637e85..5cc8585217 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -19,20 +19,10 @@ module Reline class ConfigEncodingConversionError < StandardError; end Key = Struct.new(:char, :combined_char, :with_meta) do - def match?(other) - case other - when Reline::Key - (other.char.nil? or char.nil? or char == other.char) and - (other.combined_char.nil? or combined_char.nil? or combined_char == other.combined_char) and - (other.with_meta.nil? or with_meta.nil? or with_meta == other.with_meta) - when Integer, Symbol - (combined_char and combined_char == other) or - (combined_char.nil? and char and char == other) - else - false - end + # For dialog_proc `key.match?(dialog.name)` + def match?(sym) + combined_char.is_a?(Symbol) && combined_char == sym end - alias_method :==, :match? end CursorPos = Struct.new(:x, :y) DialogRenderInfo = Struct.new( @@ -400,9 +390,8 @@ def readline(prompt = '', add_hist = false) end case result when :matched - expanded = key_stroke.expand(buffer).map{ |expanded_c| - Reline::Key.new(expanded_c, expanded_c, false) - } + expanded, rest_bytes = key_stroke.expand(buffer) + rest_bytes.reverse_each { |c| io_gate.ungetc(c) } block.(expanded) break when :matching @@ -416,10 +405,9 @@ def readline(prompt = '', add_hist = false) if buffer.size == 1 and c == "\e".ord read_escaped_key(keyseq_timeout, c, block) else - expanded = buffer.map{ |expanded_c| - Reline::Key.new(expanded_c, expanded_c, false) - } - block.(expanded) + expanded, rest_bytes = key_stroke.expand(buffer) + rest_bytes.reverse_each { |c| io_gate.ungetc(c) } + block.(expanded) if expanded end break end @@ -442,9 +430,8 @@ def readline(prompt = '', add_hist = false) return :next when :matched buffer << succ_c - expanded = key_stroke.expand(buffer).map{ |expanded_c| - Reline::Key.new(expanded_c, expanded_c, false) - } + expanded, rest_bytes = key_stroke.expand(buffer) + rest_bytes.reverse_each { |c| io_gate.ungetc(c) } block.(expanded) return :break end diff --git a/lib/reline/config.rb b/lib/reline/config.rb index 62c6d105b3..937f9912d4 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -29,18 +29,20 @@ class InvalidInputrc < RuntimeError attr_accessor :autocompletion def initialize - @additional_key_bindings = {} # from inputrc - @additional_key_bindings[:emacs] = {} - @additional_key_bindings[:vi_insert] = {} - @additional_key_bindings[:vi_command] = {} - @oneshot_key_bindings = {} + @additional_key_bindings = { # from inputrc + emacs: Reline::KeyActor::Base.new, + vi_insert: Reline::KeyActor::Base.new, + vi_command: Reline::KeyActor::Base.new + } + @oneshot_key_bindings = Reline::KeyActor::Base.new @editing_mode_label = :emacs @keymap_label = :emacs @keymap_prefix = [] - @key_actors = {} - @key_actors[:emacs] = Reline::KeyActor::Emacs.new - @key_actors[:vi_insert] = Reline::KeyActor::ViInsert.new - @key_actors[:vi_command] = Reline::KeyActor::ViCommand.new + @key_actors = { + emacs: Reline::KeyActor::Base.new(Reline::KeyActor::EMACS_MAPPING), + vi_insert: Reline::KeyActor::Base.new(Reline::KeyActor::VI_INSERT_MAPPING), + vi_command: Reline::KeyActor::Base.new(Reline::KeyActor::VI_COMMAND_MAPPING) + } @vi_cmd_mode_string = '(cmd)' @vi_ins_mode_string = '(ins)' @emacs_mode_string = '@' @@ -133,14 +135,14 @@ def read(file = nil) def key_bindings # The key bindings for each editing mode will be overwritten by the user-defined ones. - kb = @key_actors[@editing_mode_label].default_key_bindings.dup - kb.merge!(@additional_key_bindings[@editing_mode_label]) - kb.merge!(@oneshot_key_bindings) - kb + Reline::KeyActor::Composite.new([@oneshot_key_bindings, @additional_key_bindings[@editing_mode_label], @key_actors[@editing_mode_label]]) end def add_oneshot_key_binding(keystroke, target) - @oneshot_key_bindings[keystroke] = target + # IRB sets invalid keystroke [Reline::Key]. We should ignore it. + return unless keystroke.all? { |c| c.is_a?(Integer) } + + @oneshot_key_bindings.add(keystroke, target) end def reset_oneshot_key_bindings @@ -148,11 +150,11 @@ def reset_oneshot_key_bindings end def add_default_key_binding_by_keymap(keymap, keystroke, target) - @key_actors[keymap].default_key_bindings[keystroke] = target + @key_actors[keymap].add(keystroke, target) end def add_default_key_binding(keystroke, target) - @key_actors[@keymap_label].default_key_bindings[keystroke] = target + @key_actors[@keymap_label].add(keystroke, target) end def read_lines(lines, file = nil) @@ -192,7 +194,7 @@ def read_lines(lines, file = nil) func_name = func_name.split.first keystroke, func = bind_key(key, func_name) next unless keystroke - @additional_key_bindings[@keymap_label][@keymap_prefix + keystroke] = func + @additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func) end end unless if_stack.empty? diff --git a/lib/reline/key_actor.rb b/lib/reline/key_actor.rb index ebe09d2009..0ac7604556 100644 --- a/lib/reline/key_actor.rb +++ b/lib/reline/key_actor.rb @@ -2,6 +2,7 @@ module Reline::KeyActor end require 'reline/key_actor/base' +require 'reline/key_actor/composite' require 'reline/key_actor/emacs' require 'reline/key_actor/vi_command' require 'reline/key_actor/vi_insert' diff --git a/lib/reline/key_actor/base.rb b/lib/reline/key_actor/base.rb index 194e98938c..ee28c7681e 100644 --- a/lib/reline/key_actor/base.rb +++ b/lib/reline/key_actor/base.rb @@ -1,15 +1,31 @@ class Reline::KeyActor::Base - MAPPING = Array.new(256) + def initialize(mapping = []) + @mapping = mapping + @matching_bytes = {} + @key_bindings = {} + end def get_method(key) - self.class::MAPPING[key] + @mapping[key] + end + + def add(key, func) + (1...key.size).each do |size| + @matching_bytes[key.take(size)] = true + end + @key_bindings[key] = func + end + + def matching?(key) + @matching_bytes[key] end - def initialize - @default_key_bindings = {} + def get(key) + @key_bindings[key] end - def default_key_bindings - @default_key_bindings + def clear + @matching_bytes.clear + @key_bindings.clear end end diff --git a/lib/reline/key_actor/composite.rb b/lib/reline/key_actor/composite.rb new file mode 100644 index 0000000000..37e94ce6cf --- /dev/null +++ b/lib/reline/key_actor/composite.rb @@ -0,0 +1,17 @@ +class Reline::KeyActor::Composite + def initialize(key_actors) + @key_actors = key_actors + end + + def matching?(key) + @key_actors.any? { |key_actor| key_actor.matching?(key) } + end + + def get(key) + @key_actors.each do |key_actor| + func = key_actor.get(key) + return func if func + end + nil + end +end diff --git a/lib/reline/key_actor/emacs.rb b/lib/reline/key_actor/emacs.rb index d7354520b0..ad84ee1d99 100644 --- a/lib/reline/key_actor/emacs.rb +++ b/lib/reline/key_actor/emacs.rb @@ -1,5 +1,5 @@ -class Reline::KeyActor::Emacs < Reline::KeyActor::Base - MAPPING = [ +module Reline::KeyActor + EMACS_MAPPING = [ # 0 ^@ :em_set_mark, # 1 ^A diff --git a/lib/reline/key_actor/vi_command.rb b/lib/reline/key_actor/vi_command.rb index 06bb0ba8e4..d972c5e67f 100644 --- a/lib/reline/key_actor/vi_command.rb +++ b/lib/reline/key_actor/vi_command.rb @@ -1,5 +1,5 @@ -class Reline::KeyActor::ViCommand < Reline::KeyActor::Base - MAPPING = [ +module Reline::KeyActor + VI_COMMAND_MAPPING = [ # 0 ^@ :ed_unassigned, # 1 ^A diff --git a/lib/reline/key_actor/vi_insert.rb b/lib/reline/key_actor/vi_insert.rb index f8ccf468c6..312df1646b 100644 --- a/lib/reline/key_actor/vi_insert.rb +++ b/lib/reline/key_actor/vi_insert.rb @@ -1,5 +1,5 @@ -class Reline::KeyActor::ViInsert < Reline::KeyActor::Base - MAPPING = [ +module Reline::KeyActor + VI_INSERT_MAPPING = [ # 0 ^@ :ed_unassigned, # 1 ^A diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb index bceffbb53f..f87cef9968 100644 --- a/lib/reline/key_stroke.rb +++ b/lib/reline/key_stroke.rb @@ -7,139 +7,67 @@ def initialize(config) @config = config end - def compress_meta_key(ary) - return ary unless @config.convert_meta - ary.inject([]) { |result, key| - if result.size > 0 and result.last == "\e".ord - result[result.size - 1] = Reline::Key.new(key, key | 0b10000000, true) - else - result << key - end - result - } - end - - def start_with?(me, other) - compressed_me = compress_meta_key(me) - compressed_other = compress_meta_key(other) - i = 0 - loop do - my_c = compressed_me[i] - other_c = compressed_other[i] - other_is_last = (i + 1) == compressed_other.size - me_is_last = (i + 1) == compressed_me.size - if my_c != other_c - if other_c == "\e".ord and other_is_last and my_c.is_a?(Reline::Key) and my_c.with_meta - return true - else - return false - end - elsif other_is_last - return true - elsif me_is_last - return false - end - i += 1 - end - end - - def equal?(me, other) - case me - when Array - compressed_me = compress_meta_key(me) - compressed_other = compress_meta_key(other) - compressed_me.size == compressed_other.size and [compressed_me, compressed_other].transpose.all?{ |i| equal?(i[0], i[1]) } - when Integer - if other.is_a?(Reline::Key) - if other.combined_char == "\e".ord - false - else - other.combined_char == me - end - else - me == other - end - when Reline::Key - if other.is_a?(Integer) - me.combined_char == other - else - me == other - end - end - end - def match_status(input) - key_mapping.keys.select { |lhs| - start_with?(lhs, input) - }.tap { |it| - return :matched if it.size == 1 && equal?(it[0], input) - return :matching if it.size == 1 && !equal?(it[0], input) - return :matched if it.max_by(&:size)&.size&.< input.size - return :matching if it.size > 1 - } - if key_mapping.keys.any? { |lhs| start_with?(input, lhs) } + if key_mapping.matching?(input) + :matching + elsif key_mapping.get(input) + :matched + elsif input[0] == ESC_BYTE + match_unknown_escape_sequence(input, vi_mode: @config.editing_mode_is?(:vi_insert, :vi_command)) + elsif input.size == 1 :matched else - match_unknown_escape_sequence(input).first + :unmatched end end def expand(input) - lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last - unless lhs - status, size = match_unknown_escape_sequence(input) - case status - when :matched - return [:ed_unassigned] + expand(input.drop(size)) - when :matching - return [:ed_unassigned] - else - return input - end + matched_bytes = nil + (1..input.size).each do |i| + bytes = input.take(i) + matched_bytes = bytes if match_status(bytes) != :unmatched end - rhs = key_mapping[lhs] + return unless matched_bytes - case rhs - when String - rhs_bytes = rhs.bytes - expand(expand(rhs_bytes) + expand(input.drop(lhs.size))) - when Symbol - [rhs] + expand(input.drop(lhs.size)) - when Array - rhs + func = key_mapping.get(matched_bytes) + if func.is_a?(Array) + keys = func.map { |c| Reline::Key.new(c, c, false) } + elsif func + keys = [Reline::Key.new(func, func, false)] + elsif input.size == 1 + keys = [Reline::Key.new(input.first, input.first, false)] + elsif input.size == 2 && input[0] == ESC_BYTE + keys = [Reline::Key.new(input[1], input[1] | 0b10000000, true)] end + + [keys, input.drop(matched_bytes.size)] end private # returns match status of CSI/SS3 sequence and matched length - def match_unknown_escape_sequence(input) + def match_unknown_escape_sequence(input, vi_mode: false) idx = 0 - return [:unmatched, nil] unless input[idx] == ESC_BYTE + return :unmatched unless input[idx] == ESC_BYTE idx += 1 idx += 1 if input[idx] == ESC_BYTE case input[idx] when nil - return [:matching, nil] + return :matching when 91 # == '['.ord - # CSI sequence + # CSI sequence `ESC [ ... char` idx += 1 idx += 1 while idx < input.size && CSI_PARAMETER_BYTES_RANGE.cover?(input[idx]) idx += 1 while idx < input.size && CSI_INTERMEDIATE_BYTES_RANGE.cover?(input[idx]) - input[idx] ? [:matched, idx + 1] : [:matching, nil] when 79 # == 'O'.ord - # SS3 sequence - input[idx + 1] ? [:matched, idx + 2] : [:matching, nil] + # SS3 sequence `ESC O char` + idx += 1 else - if idx == 1 - # `ESC char`, make it :unmatched so that it will be handled correctly in `read_2nd_character_of_key_sequence` - [:unmatched, nil] - else - # `ESC ESC char` - [:matched, idx + 1] - end + # `ESC char` or `ESC ESC char` + return :unmatched if vi_mode end + input[idx + 1] ? :unmatched : input[idx] ? :matched : :matching end def key_mapping diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index c2f5f0622e..6038d8a4fa 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -684,10 +684,8 @@ def call(key) @trap_key.each do |t| @config.add_oneshot_key_binding(t, @name) end - elsif @trap_key.is_a?(Array) + else @config.add_oneshot_key_binding(@trap_key, @name) - elsif @trap_key.is_a?(Integer) or @trap_key.is_a?(Reline::Key) - @config.add_oneshot_key_binding([@trap_key], @name) end end dialog_render_info diff --git a/test/reline/helper.rb b/test/reline/helper.rb index a5f850e838..9a346a17a1 100644 --- a/test/reline/helper.rb +++ b/test/reline/helper.rb @@ -168,7 +168,7 @@ def assert_whole_lines(expected) def assert_key_binding(input, method_symbol, editing_modes = [:emacs, :vi_insert, :vi_command]) editing_modes.each do |editing_mode| @config.editing_mode = editing_mode - assert_equal(method_symbol, @config.editing_mode.default_key_bindings[input.bytes]) + assert_equal(method_symbol, @config.editing_mode.get(input.bytes)) end end end diff --git a/test/reline/test_config.rb b/test/reline/test_config.rb index 8c4b513600..16727c9bc9 100644 --- a/test/reline/test_config.rb +++ b/test/reline/test_config.rb @@ -22,6 +22,15 @@ def teardown @config.reset end + def additional_key_bindings(keymap_label) + @config.instance_variable_get(:@additional_key_bindings)[keymap_label].instance_variable_get(:@key_bindings) + end + + def registered_key_bindings(keys) + key_bindings = @config.key_bindings + keys.to_h { |key| [key, key_bindings.get(key)] } + end + def test_read_lines @config.read_lines(<<~LINES.lines) set bell-style on @@ -97,14 +106,17 @@ def test_encoding_is_not_ascii assert_equal nil, @config.convert_meta end - def test_comment_line - @config.read_lines([" #a: error\n"]) - assert_not_include @config.key_bindings, nil - end - def test_invalid_keystroke - @config.read_lines(["a: error\n"]) - assert_not_include @config.key_bindings, nil + @config.read_lines(<<~LINES.lines) + #"a": comment + a: error + "b": no-error + LINES + key_bindings = additional_key_bindings(:emacs) + assert_not_include key_bindings, 'a'.bytes + assert_not_include key_bindings, nil + assert_not_include key_bindings, [] + assert_include key_bindings, 'b'.bytes end def test_bind_key @@ -242,8 +254,8 @@ def test_nested_if_else "\xC": "O" LINES keys = [0x1, 0x3, 0x4, 0x6, 0x7, 0xC] - key_bindings = keys.to_h { |k| [[k.ord], ['O'.ord]] } - assert_equal(key_bindings, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) + key_bindings = keys.to_h { |k| [[k], ['O'.ord]] } + assert_equal(key_bindings, additional_key_bindings(:emacs)) end def test_unclosed_if @@ -282,9 +294,9 @@ def test_if_with_mode $endif LINES - assert_equal({[5] => :history_search_backward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + assert_equal({[5] => :history_search_backward}, additional_key_bindings(:emacs)) + assert_equal({}, additional_key_bindings(:vi_insert)) + assert_equal({}, additional_key_bindings(:vi_command)) end def test_else @@ -296,9 +308,9 @@ def test_else $endif LINES - assert_equal({[6] => :history_search_forward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + assert_equal({[6] => :history_search_forward}, additional_key_bindings(:emacs)) + assert_equal({}, additional_key_bindings(:vi_insert)) + assert_equal({}, additional_key_bindings(:vi_command)) end def test_if_with_invalid_mode @@ -310,9 +322,9 @@ def test_if_with_invalid_mode $endif LINES - assert_equal({[6] => :history_search_forward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + assert_equal({[6] => :history_search_forward}, additional_key_bindings(:emacs)) + assert_equal({}, additional_key_bindings(:vi_insert)) + assert_equal({}, additional_key_bindings(:vi_command)) end def test_mode_label_differs_from_keymap_label @@ -327,9 +339,9 @@ def test_mode_label_differs_from_keymap_label "\C-e": history-search-backward $endif LINES - assert_equal({[5] => :history_search_backward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + assert_equal({[5] => :history_search_backward}, additional_key_bindings(:emacs)) + assert_equal({}, additional_key_bindings(:vi_insert)) + assert_equal({}, additional_key_bindings(:vi_command)) end def test_if_without_else_condition @@ -340,9 +352,9 @@ def test_if_without_else_condition $endif LINES - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) - assert_equal({[5] => :history_search_backward}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) - assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + assert_equal({}, additional_key_bindings(:emacs)) + assert_equal({[5] => :history_search_backward}, additional_key_bindings(:vi_insert)) + assert_equal({}, additional_key_bindings(:vi_command)) end def test_default_key_bindings @@ -353,7 +365,7 @@ def test_default_key_bindings LINES expected = { 'abcd'.bytes => 'ABCD'.bytes, 'ijkl'.bytes => 'IJKL'.bytes } - assert_equal expected, @config.key_bindings + assert_equal expected, registered_key_bindings(expected.keys) end def test_additional_key_bindings @@ -363,7 +375,7 @@ def test_additional_key_bindings LINES expected = { 'ef'.bytes => 'EF'.bytes, 'gh'.bytes => 'GH'.bytes } - assert_equal expected, @config.key_bindings + assert_equal expected, registered_key_bindings(expected.keys) end def test_additional_key_bindings_with_nesting_and_comment_out @@ -375,7 +387,7 @@ def test_additional_key_bindings_with_nesting_and_comment_out LINES expected = { 'ef'.bytes => 'EF'.bytes, 'gh'.bytes => 'GH'.bytes } - assert_equal expected, @config.key_bindings + assert_equal expected, registered_key_bindings(expected.keys) end def test_additional_key_bindings_for_other_keymap @@ -390,7 +402,7 @@ def test_additional_key_bindings_for_other_keymap LINES expected = { 'cd'.bytes => 'CD'.bytes } - assert_equal expected, @config.key_bindings + assert_equal expected, registered_key_bindings(expected.keys) end def test_additional_key_bindings_for_auxiliary_emacs_keymaps @@ -412,7 +424,7 @@ def test_additional_key_bindings_for_auxiliary_emacs_keymaps "\C-xef".bytes => 'EF'.bytes, "\egh".bytes => 'GH'.bytes, } - assert_equal expected, @config.key_bindings + assert_equal expected, registered_key_bindings(expected.keys) end def test_key_bindings_with_reset @@ -424,7 +436,7 @@ def test_key_bindings_with_reset LINES @config.reset expected = { 'default'.bytes => 'DEFAULT'.bytes, 'additional'.bytes => 'ADDITIONAL'.bytes } - assert_equal expected, @config.key_bindings + assert_equal expected, registered_key_bindings(expected.keys) end def test_history_size diff --git a/test/reline/test_key_actor_emacs.rb b/test/reline/test_key_actor_emacs.rb index a2ea060ab9..36fbfd4040 100644 --- a/test/reline/test_key_actor_emacs.rb +++ b/test/reline/test_key_actor_emacs.rb @@ -1,6 +1,6 @@ require_relative 'helper' -class Reline::KeyActor::Emacs::Test < Reline::TestCase +class Reline::KeyActor::EmacsTest < Reline::TestCase def setup Reline.send(:test_mode) @prompt = '> ' diff --git a/test/reline/test_key_actor_vi.rb b/test/reline/test_key_actor_vi.rb index 4deae2dd83..1288b9aaa5 100644 --- a/test/reline/test_key_actor_vi.rb +++ b/test/reline/test_key_actor_vi.rb @@ -1,6 +1,6 @@ require_relative 'helper' -class Reline::KeyActor::ViInsert::Test < Reline::TestCase +class Reline::ViInsertTest < Reline::TestCase def setup Reline.send(:test_mode) @prompt = '> ' @@ -13,69 +13,73 @@ def setup @line_editor.reset(@prompt, encoding: @encoding) end + def editing_mode_label + @config.instance_variable_get(:@editing_mode_label) + end + def teardown Reline.test_reset end def test_vi_command_mode input_keys("\C-[") - assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) + assert_equal(:vi_command, editing_mode_label) end def test_vi_command_mode_with_input input_keys("abc\C-[") - assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) + assert_equal(:vi_command, editing_mode_label) assert_line_around_cursor('ab', 'c') end def test_vi_insert - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) input_keys('i') assert_line_around_cursor('i', '') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) input_keys("\C-[") assert_line_around_cursor('', 'i') - assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) + assert_equal(:vi_command, editing_mode_label) input_keys('i') assert_line_around_cursor('', 'i') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) end def test_vi_add - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) input_keys('a') assert_line_around_cursor('a', '') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) input_keys("\C-[") assert_line_around_cursor('', 'a') - assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) + assert_equal(:vi_command, editing_mode_label) input_keys('a') assert_line_around_cursor('a', '') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) end def test_vi_insert_at_bol input_keys('I') assert_line_around_cursor('I', '') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) input_keys("12345\C-[hh") assert_line_around_cursor('I12', '345') - assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) + assert_equal(:vi_command, editing_mode_label) input_keys('I') assert_line_around_cursor('', 'I12345') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) end def test_vi_add_at_eol input_keys('A') assert_line_around_cursor('A', '') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) input_keys("12345\C-[hh") assert_line_around_cursor('A12', '345') - assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) + assert_equal(:vi_command, editing_mode_label) input_keys('A') assert_line_around_cursor('A12345', '') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) end def test_ed_insert_one @@ -901,11 +905,11 @@ def test_vi_change_to_eol assert_line_around_cursor('abc', '') input_keys("\C-[0C") assert_line_around_cursor('', '') - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) end def test_vi_motion_operators - assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) + assert_equal(:vi_insert, editing_mode_label) assert_nothing_raised do input_keys("test = { foo: bar }\C-[BBBldt}b") diff --git a/test/reline/test_key_stroke.rb b/test/reline/test_key_stroke.rb index cd205c7d9e..89b3d11625 100644 --- a/test/reline/test_key_stroke.rb +++ b/test/reline/test_key_stroke.rb @@ -27,13 +27,11 @@ def test_match_status assert_equal(:matching, stroke.match_status("a".bytes)) assert_equal(:matching, stroke.match_status("ab".bytes)) assert_equal(:matched, stroke.match_status("abc".bytes)) - assert_equal(:matched, stroke.match_status("abz".bytes)) - assert_equal(:matched, stroke.match_status("abx".bytes)) - assert_equal(:matched, stroke.match_status("ac".bytes)) - assert_equal(:matched, stroke.match_status("aa".bytes)) + assert_equal(:unmatched, stroke.match_status("abz".bytes)) + assert_equal(:unmatched, stroke.match_status("abcx".bytes)) + assert_equal(:unmatched, stroke.match_status("aa".bytes)) assert_equal(:matched, stroke.match_status("x".bytes)) - assert_equal(:unmatched, stroke.match_status("m".bytes)) - assert_equal(:matched, stroke.match_status("abzwabk".bytes)) + assert_equal(:unmatched, stroke.match_status("xa".bytes)) end def test_match_unknown @@ -47,10 +45,13 @@ def test_match_unknown "\e[1;1R", # Cursor position report "\e[15~", # F5 "\eOP", # F1 - "\e\e[A" # Option+Up + "\e\e[A", # Option+Up + "\eX", + "\e\eX" ] sequences.each do |seq| assert_equal(:matched, stroke.match_status(seq.bytes)) + assert_equal(:unmatched, stroke.match_status(seq.bytes + [32])) (1...seq.size).each do |i| assert_equal(:matching, stroke.match_status(seq.bytes.take(i))) end @@ -61,16 +62,18 @@ def test_expand config = Reline::Config.new { 'abc' => '123', + 'ab' => '456' }.each_pair do |key, func| config.add_default_key_binding(key.bytes, func.bytes) end stroke = Reline::KeyStroke.new(config) - assert_equal('123'.bytes, stroke.expand('abc'.bytes)) + assert_equal(['123'.bytes.map { |c| Reline::Key.new(c, c, false) }, 'de'.bytes], stroke.expand('abcde'.bytes)) + assert_equal(['456'.bytes.map { |c| Reline::Key.new(c, c, false) }, 'de'.bytes], stroke.expand('abde'.bytes)) # CSI sequence - assert_equal([:ed_unassigned] + 'bc'.bytes, stroke.expand("\e[1;2;3;4;5abc".bytes)) - assert_equal([:ed_unassigned] + 'BC'.bytes, stroke.expand("\e\e[ABC".bytes)) + assert_equal([nil, 'bc'.bytes], stroke.expand("\e[1;2;3;4;5abc".bytes)) + assert_equal([nil, 'BC'.bytes], stroke.expand("\e\e[ABC".bytes)) # SS3 sequence - assert_equal([:ed_unassigned] + 'QR'.bytes, stroke.expand("\eOPQR".bytes)) + assert_equal([nil, 'QR'.bytes], stroke.expand("\eOPQR".bytes)) end def test_oneshot_key_bindings @@ -88,17 +91,14 @@ def test_oneshot_key_bindings def test_with_reline_key config = Reline::Config.new { - [ - Reline::Key.new(100, 228, true), # Alt+d - Reline::Key.new(97, 97, false) # a - ] => 'abc', + "\eda".bytes => 'abc', # Alt+d a [195, 164] => 'def' }.each_pair do |key, func| config.add_oneshot_key_binding(key, func.bytes) end stroke = Reline::KeyStroke.new(config) assert_equal(:unmatched, stroke.match_status('da'.bytes)) - assert_equal(:matched, stroke.match_status("\M-da".bytes)) + assert_equal(:matched, stroke.match_status("\eda".bytes)) assert_equal(:unmatched, stroke.match_status([32, 195, 164])) assert_equal(:matched, stroke.match_status([195, 164])) end diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index c664ab74b0..deff411232 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -303,12 +303,12 @@ def test_set_input_and_output def test_vi_editing_mode Reline.vi_editing_mode - assert_equal(Reline::KeyActor::ViInsert, Reline.core.config.editing_mode.class) + assert_equal(:vi_insert, Reline.core.config.instance_variable_get(:@editing_mode_label)) end def test_emacs_editing_mode Reline.emacs_editing_mode - assert_equal(Reline::KeyActor::Emacs, Reline.core.config.editing_mode.class) + assert_equal(:emacs, Reline.core.config.instance_variable_get(:@editing_mode_label)) end def test_add_dialog_proc diff --git a/test/reline/test_reline_key.rb b/test/reline/test_reline_key.rb index 7f9a11394a..1e6b9fcb6c 100644 --- a/test/reline/test_reline_key.rb +++ b/test/reline/test_reline_key.rb @@ -2,53 +2,10 @@ require "reline" class Reline::TestKey < Reline::TestCase - def setup - Reline.test_mode - end - - def teardown - Reline.test_reset - end - - def test_match_key - assert(Reline::Key.new(1, 2, false).match?(Reline::Key.new(1, 2, false))) - assert(Reline::Key.new(1, 2, false).match?(Reline::Key.new(nil, 2, false))) - assert(Reline::Key.new(1, 2, false).match?(Reline::Key.new(1, 2, nil))) - - assert(Reline::Key.new(nil, 2, false).match?(Reline::Key.new(nil, 2, false))) - assert(Reline::Key.new(1, nil, false).match?(Reline::Key.new(1, nil, false))) - assert(Reline::Key.new(1, 2, nil).match?(Reline::Key.new(1, 2, nil))) - - assert(Reline::Key.new(nil, 2, false).match?(Reline::Key.new(nil, 2, false))) - assert(Reline::Key.new(1, nil, false).match?(Reline::Key.new(1, nil, false))) - assert(Reline::Key.new(1, 2, nil).match?(Reline::Key.new(1, 2, nil))) - - assert(!Reline::Key.new(1, 2, false).match?(Reline::Key.new(3, 1, false))) - assert(!Reline::Key.new(1, 2, false).match?(Reline::Key.new(1, 3, false))) - assert(!Reline::Key.new(1, 2, false).match?(Reline::Key.new(1, 3, true))) - end - - def test_match_integer - assert(Reline::Key.new(1, 2, false).match?(2)) - assert(Reline::Key.new(nil, 2, false).match?(2)) - assert(Reline::Key.new(1, nil, false).match?(1)) - - assert(!Reline::Key.new(1, 2, false).match?(1)) - assert(!Reline::Key.new(1, nil, false).match?(2)) - assert(!Reline::Key.new(nil, nil, false).match?(1)) - end - def test_match_symbol - assert(Reline::Key.new(:key1, :key2, false).match?(:key2)) - assert(Reline::Key.new(:key1, nil, false).match?(:key1)) - - assert(!Reline::Key.new(:key1, :key2, false).match?(:key1)) - assert(!Reline::Key.new(:key1, nil, false).match?(:key2)) - assert(!Reline::Key.new(nil, nil, false).match?(:key1)) - end - - def test_match_other - assert(!Reline::Key.new(:key1, 2, false).match?("key1")) - assert(!Reline::Key.new(nil, nil, false).match?(nil)) + assert(Reline::Key.new(:key1, :key1, false).match?(:key1)) + refute(Reline::Key.new(:key1, :key1, false).match?(:key2)) + refute(Reline::Key.new(:key1, :key1, false).match?(nil)) + refute(Reline::Key.new(1, 1, false).match?(:key1)) end end From 7251d1992cae3f798b15a6221683d787303b7fca Mon Sep 17 00:00:00 2001 From: tompng Date: Sun, 2 Jun 2024 22:53:17 +0900 Subject: [PATCH 2/4] Rename key_actors to default_key_bindings --- lib/reline/config.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/reline/config.rb b/lib/reline/config.rb index 937f9912d4..e74975a95c 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -38,7 +38,7 @@ def initialize @editing_mode_label = :emacs @keymap_label = :emacs @keymap_prefix = [] - @key_actors = { + @default_key_bindings = { emacs: Reline::KeyActor::Base.new(Reline::KeyActor::EMACS_MAPPING), vi_insert: Reline::KeyActor::Base.new(Reline::KeyActor::VI_INSERT_MAPPING), vi_command: Reline::KeyActor::Base.new(Reline::KeyActor::VI_COMMAND_MAPPING) @@ -64,7 +64,7 @@ def reset end def editing_mode - @key_actors[@editing_mode_label] + @default_key_bindings[@editing_mode_label] end def editing_mode=(val) @@ -76,7 +76,7 @@ def editing_mode_is?(*val) end def keymap - @key_actors[@keymap_label] + @default_key_bindings[@keymap_label] end def loaded? @@ -135,7 +135,7 @@ def read(file = nil) def key_bindings # The key bindings for each editing mode will be overwritten by the user-defined ones. - Reline::KeyActor::Composite.new([@oneshot_key_bindings, @additional_key_bindings[@editing_mode_label], @key_actors[@editing_mode_label]]) + Reline::KeyActor::Composite.new([@oneshot_key_bindings, @additional_key_bindings[@editing_mode_label], @default_key_bindings[@editing_mode_label]]) end def add_oneshot_key_binding(keystroke, target) @@ -150,11 +150,11 @@ def reset_oneshot_key_bindings end def add_default_key_binding_by_keymap(keymap, keystroke, target) - @key_actors[keymap].add(keystroke, target) + @default_key_bindings[keymap].add(keystroke, target) end def add_default_key_binding(keystroke, target) - @key_actors[@keymap_label].add(keystroke, target) + @default_key_bindings[@keymap_label].add(keystroke, target) end def read_lines(lines, file = nil) From 34d39c1dad3fc1b4eae579b9d114a79d9b3bd2a5 Mon Sep 17 00:00:00 2001 From: tompng Date: Sun, 2 Jun 2024 23:13:06 +0900 Subject: [PATCH 3/4] Make key_stroke.expand always return a value --- lib/reline.rb | 2 +- lib/reline/key_stroke.rb | 12 +++++++----- test/reline/test_key_stroke.rb | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/reline.rb b/lib/reline.rb index 5cc8585217..33a1cfc625 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -407,7 +407,7 @@ def readline(prompt = '', add_hist = false) else expanded, rest_bytes = key_stroke.expand(buffer) rest_bytes.reverse_each { |c| io_gate.ungetc(c) } - block.(expanded) if expanded + block.(expanded) end break end diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb index f87cef9968..419ddd8cea 100644 --- a/lib/reline/key_stroke.rb +++ b/lib/reline/key_stroke.rb @@ -27,17 +27,19 @@ def expand(input) bytes = input.take(i) matched_bytes = bytes if match_status(bytes) != :unmatched end - return unless matched_bytes + return [[], []] unless matched_bytes func = key_mapping.get(matched_bytes) if func.is_a?(Array) keys = func.map { |c| Reline::Key.new(c, c, false) } elsif func keys = [Reline::Key.new(func, func, false)] - elsif input.size == 1 - keys = [Reline::Key.new(input.first, input.first, false)] - elsif input.size == 2 && input[0] == ESC_BYTE - keys = [Reline::Key.new(input[1], input[1] | 0b10000000, true)] + elsif matched_bytes.size == 1 + keys = [Reline::Key.new(matched_bytes.first, matched_bytes.first, false)] + elsif matched_bytes.size == 2 && matched_bytes[0] == ESC_BYTE + keys = [Reline::Key.new(matched_bytes[1], matched_bytes[1] | 0b10000000, true)] + else + keys = [] end [keys, input.drop(matched_bytes.size)] diff --git a/test/reline/test_key_stroke.rb b/test/reline/test_key_stroke.rb index 89b3d11625..de67c7ad44 100644 --- a/test/reline/test_key_stroke.rb +++ b/test/reline/test_key_stroke.rb @@ -70,10 +70,10 @@ def test_expand assert_equal(['123'.bytes.map { |c| Reline::Key.new(c, c, false) }, 'de'.bytes], stroke.expand('abcde'.bytes)) assert_equal(['456'.bytes.map { |c| Reline::Key.new(c, c, false) }, 'de'.bytes], stroke.expand('abde'.bytes)) # CSI sequence - assert_equal([nil, 'bc'.bytes], stroke.expand("\e[1;2;3;4;5abc".bytes)) - assert_equal([nil, 'BC'.bytes], stroke.expand("\e\e[ABC".bytes)) + assert_equal([[], 'bc'.bytes], stroke.expand("\e[1;2;3;4;5abc".bytes)) + assert_equal([[], 'BC'.bytes], stroke.expand("\e\e[ABC".bytes)) # SS3 sequence - assert_equal([nil, 'QR'.bytes], stroke.expand("\eOPQR".bytes)) + assert_equal([[], 'QR'.bytes], stroke.expand("\eOPQR".bytes)) end def test_oneshot_key_bindings From 346e2da12f7e45cb1a73204e23aff0fe43a60f1b Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Mon, 3 Jun 2024 20:31:59 +0900 Subject: [PATCH 4/4] Update add_default_key_binding to use a add_default_key_binding_by_keymap internally Co-authored-by: Stan Lo --- lib/reline/config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/reline/config.rb b/lib/reline/config.rb index e74975a95c..774b06b6fd 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -154,7 +154,7 @@ def add_default_key_binding_by_keymap(keymap, keystroke, target) end def add_default_key_binding(keystroke, target) - @default_key_bindings[@keymap_label].add(keystroke, target) + add_default_key_binding_by_keymap(@keymap_label, keystroke, target) end def read_lines(lines, file = nil)