diff options
-rw-r--r-- | lib/reline/key_stroke.rb | 57 | ||||
-rw-r--r-- | lib/reline/line_editor.rb | 15 | ||||
-rw-r--r-- | test/reline/test_key_stroke.rb | 26 | ||||
-rw-r--r-- | test/reline/yamatanooroti/test_rendering.rb | 14 |
4 files changed, 100 insertions, 12 deletions
diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb index c1c61513a9..bceffbb53f 100644 --- a/lib/reline/key_stroke.rb +++ b/lib/reline/key_stroke.rb @@ -1,4 +1,8 @@ class Reline::KeyStroke + ESC_BYTE = 27 + CSI_PARAMETER_BYTES_RANGE = 0x30..0x3f + CSI_INTERMEDIATE_BYTES_RANGE = (0x20..0x2f) + def initialize(config) @config = config end @@ -73,17 +77,26 @@ class Reline::KeyStroke return :matched if it.max_by(&:size)&.size&.< input.size return :matching if it.size > 1 } - key_mapping.keys.select { |lhs| - start_with?(input, lhs) - }.tap { |it| - return it.size > 0 ? :matched : :unmatched - } + if key_mapping.keys.any? { |lhs| start_with?(input, lhs) } + :matched + else + match_unknown_escape_sequence(input).first + end end def expand(input) - input = compress_meta_key(input) lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last - return input unless lhs + 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 + end rhs = key_mapping[lhs] case rhs @@ -99,6 +112,36 @@ class Reline::KeyStroke private + # returns match status of CSI/SS3 sequence and matched length + def match_unknown_escape_sequence(input) + idx = 0 + return [:unmatched, nil] unless input[idx] == ESC_BYTE + idx += 1 + idx += 1 if input[idx] == ESC_BYTE + + case input[idx] + when nil + return [:matching, nil] + when 91 # == '['.ord + # CSI sequence + 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] + 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 + end + end + def key_mapping @config.key_bindings end diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 4030902139..7ad56a39f8 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -1514,11 +1514,13 @@ class Reline::LineEditor return if key.char >= 128 # maybe, first byte of multi byte method_symbol = @config.editing_mode.get_method(key.combined_char) if key.with_meta and method_symbol == :ed_unassigned - # split ESC + key - method_symbol = @config.editing_mode.get_method("\e".ord) - process_key("\e".ord, method_symbol) - method_symbol = @config.editing_mode.get_method(key.char) - process_key(key.char, method_symbol) + if @config.editing_mode_is?(:vi_command, :vi_insert) + # split ESC + key in vi mode + method_symbol = @config.editing_mode.get_method("\e".ord) + process_key("\e".ord, method_symbol) + method_symbol = @config.editing_mode.get_method(key.char) + process_key(key.char, method_symbol) + end else process_key(key.combined_char, method_symbol) end @@ -3290,4 +3292,7 @@ class Reline::LineEditor @mark_pointer = new_pointer end alias_method :exchange_point_and_mark, :em_exchange_mark + + private def em_meta_next(key) + end end diff --git a/test/reline/test_key_stroke.rb b/test/reline/test_key_stroke.rb index d377a1e972..cd205c7d9e 100644 --- a/test/reline/test_key_stroke.rb +++ b/test/reline/test_key_stroke.rb @@ -36,6 +36,27 @@ class Reline::KeyStroke::Test < Reline::TestCase assert_equal(:matched, stroke.match_status("abzwabk".bytes)) end + def test_match_unknown + config = Reline::Config.new + config.add_default_key_binding("\e[9abc".bytes, 'x') + stroke = Reline::KeyStroke.new(config) + sequences = [ + "\e[9abc", + "\e[9d", + "\e[A", # Up + "\e[1;1R", # Cursor position report + "\e[15~", # F5 + "\eOP", # F1 + "\e\e[A" # Option+Up + ] + sequences.each do |seq| + assert_equal(:matched, stroke.match_status(seq.bytes)) + (1...seq.size).each do |i| + assert_equal(:matching, stroke.match_status(seq.bytes.take(i))) + end + end + end + def test_expand config = Reline::Config.new { @@ -45,6 +66,11 @@ class Reline::KeyStroke::Test < Reline::TestCase end stroke = Reline::KeyStroke.new(config) assert_equal('123'.bytes, stroke.expand('abc'.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)) + # SS3 sequence + assert_equal([:ed_unassigned] + 'QR'.bytes, stroke.expand("\eOPQR".bytes)) end def test_oneshot_key_bindings diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index def14edbe4..c0af918844 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -237,6 +237,20 @@ begin EOC end + def test_esc_input + omit if Reline::IOGate.win? + start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') + write("def\C-aabc") + write("\e") # single ESC + sleep 1 + write("A") + write("B\eAC") # ESC + A (M-A, specified ed_unassigned in Reline::KeyActor::Emacs) + assert_screen(<<~EOC) + Multiline REPL. + prompt> abcABCdef + EOC + end + def test_prompt_with_escape_sequence ENV['RELINE_TEST_PROMPT'] = "\1\e[30m\2prompt> \1\e[m\2" start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') |