Skip to content

Commit

Permalink
Ignore unhandled escape sequences (#522)
Browse files Browse the repository at this point in the history
* Add unassigned escape sequence matcher to KeyStroke

* Do not insert ESC and unassigned ESC+key to input buffer
  • Loading branch information
tompng committed Jul 8, 2023
1 parent 8dc1e04 commit 0b63045
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 12 deletions.
57 changes: 50 additions & 7 deletions lib/reline/key_stroke.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -73,17 +77,26 @@ def match_status(input)
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
Expand All @@ -99,6 +112,36 @@ def expand(input)

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
Expand Down
15 changes: 10 additions & 5 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1514,11 +1514,13 @@ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
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
Expand Down Expand Up @@ -3290,4 +3292,7 @@ def finish
@mark_pointer = new_pointer
end
alias_method :exchange_point_and_mark, :em_exchange_mark

private def em_meta_next(key)
end
end
26 changes: 26 additions & 0 deletions test/reline/test_key_stroke.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ def test_match_status
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
{
Expand All @@ -45,6 +66,11 @@ def test_expand
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
Expand Down
14 changes: 14 additions & 0 deletions test/reline/yamatanooroti/test_rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,20 @@ def test_mode_string_vi_changing
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.')
Expand Down

0 comments on commit 0b63045

Please sign in to comment.