Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore unhandled escape sequences #522

Merged
merged 2 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F1, F2, F3, F4 key in macOS ["\eOP", "\eOQ", "\eOR", "\eOS"]

There are many other type of escape sequence, but I think it's CSI and SS3 is enough for keyboard input sequence. some other sequence ends with "\a" or "\e\\" (example: "\e]0;NewWindowTitle\e\\") but if we implement it and user mistakenly input ESC+], irb looks hanging up.

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 @@ -1571,11 +1571,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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used in vi mode.
In emacs mode, this is making ESC+char inserted into input buffer

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 @@ -3345,4 +3347,7 @@ def finish
@mark_pointer = new_pointer
end
alias_method :exchange_point_and_mark, :em_exchange_mark

private def em_meta_next(key)
end
Copy link
Member Author

@tompng tompng Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In method process_key, ed_insert is called if method_symbol is not defined.
I think adding this method is the simplest and easiest way to prevent ESC insertion.
Although, not all method_symbol written in lib/reline/key_actor/emacs.rb is defined in line_aditor.rb and I think it is not causing problem. Perhaps there is another way to fix without defining this method.

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